Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<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>