/ src / renderer / components / pages / McpAddPage.vue
McpAddPage.vue
  1  <script setup lang="ts">
  2  import { ref, watch } from 'vue'
  3  import { useStdioStore, CustomStdioServerParameters, StdioServerKey } from '@/renderer/store/stdio'
  4  
  5  import ConfigJsonCard from '@/renderer/components/common/ConfigJsonCard.vue'
  6  import { pickBy } from 'lodash'
  7  import { useSnackbarStore } from '@/renderer/store/snackbar'
  8  
  9  type McpServerConfig = {
 10    mcpServers: McpServerObject
 11  }
 12  
 13  type McpServerObject = Record<string, CustomStdioServerParameters>
 14  
 15  const snackbarStore = useSnackbarStore()
 16  
 17  const stdioStore = useStdioStore()
 18  
 19  const props = defineProps({
 20    modelValue: {
 21      type: Boolean,
 22      required: true
 23    }
 24  })
 25  
 26  const emit = defineEmits(['update:modelValue'])
 27  
 28  const internalDialog = ref(props.modelValue)
 29  
 30  watch(
 31    () => props.modelValue,
 32    (newVal) => {
 33      internalDialog.value = newVal
 34    }
 35  )
 36  
 37  watch(internalDialog, (newVal) => {
 38    emit('update:modelValue', newVal)
 39  })
 40  
 41  const closeDialog = () => {
 42    internalDialog.value = false
 43  }
 44  
 45  function findConfig(jsonConfig: McpServerConfig | McpServerObject): CustomStdioServerParameters {
 46    if (!jsonConfig) return {}
 47  
 48    const mcpConfig = jsonConfig.mcpServers ? jsonConfig.mcpServers : jsonConfig
 49  
 50    const filtered = pickBy(
 51      mcpConfig,
 52      (value) => value && typeof value === 'object' && 'command' in value
 53    )
 54  
 55    return filtered
 56  }
 57  
 58  const addConfig = () => {
 59    const jsonConfigs = findConfig(jsonParams.value)
 60  
 61    if (Object.keys(jsonConfigs).length === 0) {
 62      snackbarStore.showErrorMessage('snackbar.no-mcp-config')
 63      return
 64    }
 65  
 66    for (const [serverName, serverConfig] of Object.entries(jsonConfigs)) {
 67      const config = serverConfig as CustomStdioServerParameters
 68      ;(['command', 'args', 'env'] as StdioServerKey[]).forEach((key) => {
 69        const value = config[key]
 70        if (value) {
 71          stdioStore.updateConfigAttribute(serverName, key, value)
 72        }
 73      })
 74    }
 75  
 76    closeDialog()
 77  }
 78  
 79  const jsonError = ref<string | null>(null)
 80  
 81  function handleError(errorMessage: string | null) {
 82    jsonError.value = errorMessage
 83  }
 84  
 85  const jsonParams = ref({})
 86  
 87  const exampleData = [
 88    {
 89      'sequential-thinking': {
 90        command: 'npx',
 91        args: ['-y', '@modelcontextprotocol/server-sequential-thinking']
 92      }
 93    },
 94    {
 95      'Time Server': {
 96        command: 'uvx',
 97        args: ['mcp-server-time']
 98      },
 99      memory: {
100        command: 'npx',
101        args: ['-y', '@modelcontextprotocol/server-memory']
102      }
103    },
104    {
105      mcpServers: {
106        filesystem: {
107          command: 'npx',
108          args: [
109            '-y',
110            '@modelcontextprotocol/server-filesystem',
111            '/Users/username/Desktop',
112            '/path/to/other/allowed/dir'
113          ]
114        }
115      }
116    }
117  ] as const
118  </script>
119  
120  <template>
121    <v-dialog v-model="internalDialog" persistent max-width="80vw" max-height="80vh" scrollable>
122      <v-card :title="$t('mcp.config')">
123        <v-divider></v-divider>
124        <v-card-text>
125          <ConfigJsonCard v-model="jsonParams" @on-error="handleError"></ConfigJsonCard>
126          <div class="mx-4">
127            <v-expansion-panels static color="grey-200">
128              <v-expansion-panel
129                v-for="(value, index) in exampleData"
130                :key="index"
131                :title="`${$t('general.example')} ${index + 1}`"
132              >
133                <v-expansion-panel-text>
134                  <v-textarea
135                    :model-value="JSON.stringify(value, null, 2)"
136                    variant="plain"
137                    auto-grow
138                    hide-details
139                  ></v-textarea>
140                </v-expansion-panel-text>
141              </v-expansion-panel>
142            </v-expansion-panels>
143          </div>
144        </v-card-text>
145  
146        <v-divider></v-divider>
147        <v-card-actions>
148          <v-spacer></v-spacer>
149          <v-btn
150            variant="plain"
151            rounded="lg"
152            icon="mdi-close-box"
153            color="error"
154            @click="closeDialog"
155          ></v-btn>
156          <v-btn
157            variant="plain"
158            rounded="lg"
159            :disabled="Boolean(jsonError ?? '')"
160            icon="mdi-content-save-plus"
161            color="success"
162            @click="addConfig()"
163          ></v-btn>
164        </v-card-actions>
165      </v-card>
166    </v-dialog>
167  </template>
168  <style scoped>
169  .v-expansion-panel-parent {
170    overflow-y: auto;
171  }
172  </style>