Hello from MCP server
<template>
<div class="store-function-caller">
<!-- Function List -->
<div class="functions-block">
<h3>Functions</h3>
<div class="functions-list">
<code
v-for="fn in functionList"
:key="fn.name"
:class="{ active: selectedFunction?.name === fn.name }"
@click="selectFunction(fn)"
>
{{ fn.name }}()
</code>
</div>
</div>
<!-- Function Call Form -->
<div v-if="selectedFunction" class="function-form">
<h3>{{ selectedFunction.name }}()</h3>
<p v-if="selectedFunction.description" class="function-description">
{{ selectedFunction.description }}
</p>
<!-- Parameter Inputs -->
<div v-if="selectedFunction.params && selectedFunction.params.length > 0" class="params-list">
<div v-for="param in selectedFunction.params" :key="param.name" class="param-row">
<label :for="param.name">{{ param.name }}<span v-if="param.required" class="required">*</span></label>
<input
v-if="param.type === 'string' || param.type === 'number'"
:id="param.name"
v-model="paramValues[param.name]"
:type="param.type === 'number' ? 'number' : 'text'"
:placeholder="param.placeholder || param.name"
/>
<select
v-else-if="param.type === 'boolean'"
:id="param.name"
v-model="paramValues[param.name]"
>
<option :value="undefined">-- select --</option>
<option :value="true">true</option>
<option :value="false">false</option>
</select>
<textarea
v-else-if="param.type === 'json'"
:id="param.name"
v-model="paramValues[param.name]"
:placeholder="param.placeholder || 'JSON object'"
rows="3"
></textarea>
<span v-if="param.hint" class="param-hint">{{ param.hint }}</span>
</div>
</div>
<p v-else class="no-params">No parameters required</p>
<!-- Submit Button -->
<div class="form-actions">
<button type="button" @click="executeFunction" :disabled="isExecuting">
{{ isExecuting ? 'Running...' : 'Submit' }}
</button>
<button type="button" class="cancel-btn" @click="clearSelection">
Cancel
</button>
</div>
<!-- Error Display -->
<div v-if="error" class="error-block">
<strong>Error:</strong> {{ error }}
</div>
<!-- Result Display -->
<div v-if="result !== undefined && !error" class="result-block">
<div class="result-header">
<h4>Result</h4>
<div class="json-controls">
<button type="button" @click="expandAll">Expand All</button>
<button type="button" @click="collapseAll">Collapse All</button>
</div>
</div>
<pre><code><json-node
:key="jsonKey"
:data="result"
:indent="0"
:start-collapsed="!jsonExpandAll"
:expand-all="jsonExpandMode"
/></code></pre>
</div>
</div>
</div>
</template>
<script lang="ts">
import { ref, reactive, defineComponent, type PropType } from "vue";
import JsonNode from "@/components/JsonViewer.vue";
export interface FunctionParam {
name: string;
type: 'string' | 'number' | 'boolean' | 'json';
required?: boolean;
placeholder?: string;
hint?: string;
}
export interface FunctionDefinition {
name: string;
description?: string;
params?: FunctionParam[];
handler: (...args: any[]) => any;
}
export default defineComponent({
name: "StoreFunctionCaller",
components: { JsonNode },
props: {
functionList: {
type: Array as PropType<FunctionDefinition[]>,
required: true,
},
},
emits: ['function-executed'],
setup(props, { emit }) {
const selectedFunction = ref<FunctionDefinition | null>(null);
const paramValues = reactive<Record<string, any>>({});
const result = ref<any>(undefined);
const error = ref<string | null>(null);
const isExecuting = ref(false);
const jsonExpandAll = ref(true);
const jsonExpandMode = ref(false);
const jsonKey = ref(0);
function selectFunction(fn: FunctionDefinition) {
selectedFunction.value = fn;
// Reset param values
Object.keys(paramValues).forEach(key => delete paramValues[key]);
result.value = undefined;
error.value = null;
}
function clearSelection() {
selectedFunction.value = null;
Object.keys(paramValues).forEach(key => delete paramValues[key]);
result.value = undefined;
error.value = null;
}
async function executeFunction() {
if (!selectedFunction.value) return;
error.value = null;
result.value = undefined;
isExecuting.value = true;
try {
// Build arguments array from param values
const args: any[] = [];
for (const param of selectedFunction.value.params || []) {
let value = paramValues[param.name];
// Parse JSON params
if (param.type === 'json' && typeof value === 'string' && value.trim()) {
try {
value = JSON.parse(value);
} catch (e) {
throw new Error(`Invalid JSON for parameter "${param.name}"`);
}
}
// Convert number params
if (param.type === 'number' && value !== undefined && value !== '') {
value = Number(value);
}
args.push(value);
}
// Execute the function
const fnResult = await selectedFunction.value.handler(...args);
result.value = fnResult ?? { success: true, message: 'Function executed (no return value)' };
// Emit event so parent can refresh data
emit('function-executed', selectedFunction.value.name);
} catch (e: any) {
error.value = e.message || String(e);
} finally {
isExecuting.value = false;
}
}
function expandAll() {
jsonExpandAll.value = true;
jsonExpandMode.value = true;
jsonKey.value++;
}
function collapseAll() {
jsonExpandAll.value = false;
jsonExpandMode.value = true;
jsonKey.value++;
}
return {
selectedFunction,
paramValues,
result,
error,
isExecuting,
jsonExpandAll,
jsonExpandMode,
jsonKey,
selectFunction,
clearSelection,
executeFunction,
expandAll,
collapseAll,
};
},
});
</script>
<style scoped>
.store-function-caller {
font-family: system-ui, sans-serif;
}
.functions-block {
margin-top: 16px;
padding: 12px;
background: var(--ion-background-color-step-50, #f5f5f5);
border: 1px solid var(--ion-color-step-200, #ddd);
border-radius: 4px;
}
.functions-block h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: var(--ion-text-color);
}
.functions-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.functions-list code {
padding: 4px 8px;
background: var(--ion-background-color-step-100, #e8e8e8);
border-radius: 4px;
font-size: 12px;
color: var(--ion-color-primary);
cursor: pointer;
transition: all 0.15s ease;
}
.functions-list code:hover {
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
}
.functions-list code.active {
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
}
.function-form {
margin-top: 16px;
padding: 16px;
background: var(--ion-background-color-step-50, #f5f5f5);
border: 1px solid var(--ion-color-primary);
border-radius: 4px;
}
.function-form h3 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
color: var(--ion-color-primary);
font-family: monospace;
}
.function-description {
margin: 0 0 16px 0;
font-size: 13px;
color: var(--ion-color-medium);
}
.params-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.param-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.param-row label {
font-size: 13px;
font-weight: 500;
color: var(--ion-text-color);
}
.param-row .required {
color: var(--ion-color-danger);
margin-left: 2px;
}
.param-row input,
.param-row select,
.param-row textarea {
padding: 8px 12px;
font-size: 14px;
border: 1px solid var(--ion-color-step-300, #ccc);
border-radius: 4px;
background: var(--ion-background-color, #fff);
color: var(--ion-text-color);
}
.param-row textarea {
font-family: monospace;
resize: vertical;
}
.param-hint {
font-size: 11px;
color: var(--ion-color-medium);
}
.no-params {
margin: 0 0 16px 0;
font-size: 13px;
color: var(--ion-color-medium);
font-style: italic;
}
.form-actions {
display: flex;
gap: 8px;
}
.form-actions button {
padding: 8px 20px;
font-size: 14px;
cursor: pointer;
border: 1px solid var(--ion-color-step-300, #ccc);
border-radius: 4px;
}
.form-actions button:first-child {
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
border-color: var(--ion-color-primary);
}
.form-actions button:first-child:hover {
background: var(--ion-color-primary-shade);
}
.form-actions button:first-child:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.form-actions .cancel-btn {
background: var(--ion-background-color-step-100, #f0f0f0);
color: var(--ion-text-color);
}
.form-actions .cancel-btn:hover {
background: var(--ion-background-color-step-150, #e0e0e0);
}
.error-block {
margin-top: 16px;
padding: 12px;
background: var(--ion-color-danger-tint, #ffeaea);
border: 1px solid var(--ion-color-danger);
border-radius: 4px;
color: var(--ion-color-danger);
font-size: 13px;
}
.result-block {
margin-top: 16px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.result-header h4 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--ion-text-color);
}
.json-controls {
display: flex;
gap: 8px;
}
.json-controls button {
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
background: var(--ion-background-color-step-100, #f0f0f0);
border: 1px solid var(--ion-color-step-300, #ccc);
color: var(--ion-text-color);
border-radius: 4px;
}
.json-controls button:hover {
background: var(--ion-background-color-step-150, #e0e0e0);
}
.result-block pre {
margin: 0;
padding: 12px;
background: var(--ion-background-color, #fff);
border: 1px solid var(--ion-color-step-200, #ddd);
border-radius: 4px;
overflow-x: auto;
}
.result-block code {
font-family: monospace;
font-size: 12px;
white-space: pre;
color: var(--ion-text-color);
}
</style>