@lobehub/chat 1.82.1 → 1.82.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/plugin.json +25 -5
- package/locales/bg-BG/plugin.json +25 -5
- package/locales/de-DE/plugin.json +30 -5
- package/locales/en-US/plugin.json +30 -5
- package/locales/es-ES/plugin.json +25 -5
- package/locales/fa-IR/plugin.json +25 -5
- package/locales/fr-FR/plugin.json +25 -5
- package/locales/it-IT/plugin.json +25 -5
- package/locales/ja-JP/plugin.json +25 -5
- package/locales/ko-KR/plugin.json +30 -5
- package/locales/nl-NL/plugin.json +25 -5
- package/locales/pl-PL/plugin.json +30 -5
- package/locales/pt-BR/plugin.json +25 -5
- package/locales/ru-RU/plugin.json +25 -5
- package/locales/tr-TR/plugin.json +25 -5
- package/locales/vi-VN/plugin.json +30 -5
- package/locales/zh-CN/plugin.json +23 -8
- package/locales/zh-TW/plugin.json +25 -5
- package/package.json +1 -1
- package/src/config/aiModels/openrouter.ts +6 -6
- package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +1 -1
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +160 -97
- package/src/features/PluginDevModal/MCPManifestForm/utils.test.ts +262 -0
- package/src/features/PluginDevModal/MCPManifestForm/utils.ts +151 -0
- package/src/features/PluginDevModal/index.tsx +31 -22
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +1 -1
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +160 -0
- package/src/libs/agent-runtime/utils/streams/openai.ts +1 -1
- package/src/locales/default/plugin.ts +16 -3
- package/src/server/services/mcp/index.ts +7 -0
@@ -7,21 +7,22 @@ import {
|
|
7
7
|
SiPython,
|
8
8
|
} from '@icons-pack/react-simple-icons';
|
9
9
|
import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
|
10
|
-
import {
|
11
|
-
import { AutoComplete, Form, FormInstance, Input } from 'antd';
|
12
|
-
import { FileCode
|
13
|
-
import { FC, useState } from 'react';
|
10
|
+
import { Alert, FormItem, Icon } from '@lobehub/ui';
|
11
|
+
import { AutoComplete, Button, Form, FormInstance, Input } from 'antd';
|
12
|
+
import { FileCode } from 'lucide-react';
|
13
|
+
import { ChangeEvent, FC, useState } from 'react';
|
14
14
|
import { useTranslation } from 'react-i18next';
|
15
15
|
import { Flexbox } from 'react-layout-kit';
|
16
16
|
|
17
17
|
import ManifestPreviewer from '@/components/ManifestPreviewer';
|
18
|
+
import { isDesktop } from '@/const/version';
|
18
19
|
import { mcpService } from '@/services/mcp';
|
19
20
|
import { useToolStore } from '@/store/tool';
|
20
21
|
import { pluginSelectors } from '@/store/tool/selectors';
|
21
|
-
import { PluginInstallError } from '@/types/tool/plugin';
|
22
22
|
|
23
23
|
import ArgsInput from './ArgsInput';
|
24
24
|
import MCPTypeSelect from './MCPTypeSelect';
|
25
|
+
import { parseMcpInput } from './utils';
|
25
26
|
|
26
27
|
interface MCPManifestFormProps {
|
27
28
|
form: FormInstance;
|
@@ -47,15 +48,120 @@ const STDIO_COMMAND_OPTIONS: {
|
|
47
48
|
{ color: '#2496ED', icon: SiDocker, value: 'docker' },
|
48
49
|
];
|
49
50
|
|
51
|
+
const HTTP_URL_KEY = ['customParams', 'mcp', 'url'];
|
52
|
+
const STDIO_COMMAND = ['customParams', 'mcp', 'command'];
|
53
|
+
const STDIO_ARGS = ['customParams', 'mcp', 'args'];
|
54
|
+
const MCP_TYPE = ['customParams', 'mcp', 'type'];
|
55
|
+
|
50
56
|
const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
51
57
|
const { t } = useTranslation('plugin');
|
52
|
-
const mcpType = Form.useWatch(
|
58
|
+
const mcpType = Form.useWatch(MCP_TYPE, form);
|
53
59
|
const [manifest, setManifest] = useState<LobeChatPluginManifest>();
|
54
60
|
const pluginIds = useToolStore(pluginSelectors.storeAndInstallPluginsIdList);
|
61
|
+
const [pasteError, setPasteError] = useState<string | null>(null);
|
62
|
+
const [isTesting, setIsTesting] = useState(false);
|
63
|
+
const [connectionError, setConnectionError] = useState<string | null>(null);
|
64
|
+
|
65
|
+
const handleIdentifierChange = (e: ChangeEvent<HTMLInputElement>) => {
|
66
|
+
const value = e.target.value.trim();
|
67
|
+
setPasteError(null); // Clear previous errors on new input
|
68
|
+
setConnectionError(null); // Clear connection error on identifier change
|
69
|
+
|
70
|
+
const parseResult = parseMcpInput(value);
|
71
|
+
|
72
|
+
if (parseResult.status !== 'success') return;
|
73
|
+
|
74
|
+
const { identifier, mcpConfig } = parseResult;
|
75
|
+
|
76
|
+
if (!isDesktop && mcpConfig.type === 'stdio') {
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
|
80
|
+
// Check for duplicate identifier (only in create mode)
|
81
|
+
if (!isEditMode && pluginIds.includes(identifier)) {
|
82
|
+
setPasteError(t('dev.meta.identifier.errorDuplicate'));
|
83
|
+
// Update form fields even if duplicate, so user sees the pasted values
|
84
|
+
form.setFieldsValue({
|
85
|
+
// Update identifier field
|
86
|
+
customParams: {
|
87
|
+
mcp: mcpConfig, // Spread the parsed config (includes type)
|
88
|
+
},
|
89
|
+
identifier: identifier,
|
90
|
+
});
|
91
|
+
// Trigger validation to show Form.Item error
|
92
|
+
form.validateFields(['identifier']);
|
93
|
+
return;
|
94
|
+
}
|
95
|
+
|
96
|
+
// No duplicate or in edit mode, fill the form
|
97
|
+
form.setFieldsValue({
|
98
|
+
customParams: { mcp: mcpConfig },
|
99
|
+
identifier: identifier,
|
100
|
+
});
|
101
|
+
|
102
|
+
// Clear potential old validation error on identifier
|
103
|
+
form.setFields([{ errors: [], name: 'identifier' }]);
|
104
|
+
};
|
105
|
+
|
106
|
+
const handleTestConnection = async () => {
|
107
|
+
setIsTesting(true);
|
108
|
+
setConnectionError(null);
|
109
|
+
setManifest(undefined); // Reset manifest before testing
|
110
|
+
|
111
|
+
// Manually trigger validation for fields needed for the test
|
112
|
+
let isValid = false;
|
113
|
+
try {
|
114
|
+
await form.validateFields([
|
115
|
+
...(mcpType === 'http' ? [HTTP_URL_KEY] : [STDIO_COMMAND, STDIO_ARGS]),
|
116
|
+
]);
|
117
|
+
isValid = true;
|
118
|
+
} catch {}
|
119
|
+
|
120
|
+
if (!isValid) {
|
121
|
+
setIsTesting(false);
|
122
|
+
return;
|
123
|
+
}
|
124
|
+
|
125
|
+
try {
|
126
|
+
const values = form.getFieldsValue();
|
127
|
+
const id = values.identifier;
|
128
|
+
const mcp = values.customParams?.mcp;
|
129
|
+
|
130
|
+
let data: LobeChatPluginManifest;
|
131
|
+
|
132
|
+
if (mcp.type === 'http') {
|
133
|
+
if (!mcp.url) throw new Error(t('dev.mcp.url.required'));
|
134
|
+
data = await mcpService.getStreamableMcpServerManifest(id, mcp.url);
|
135
|
+
} else if (mcp.type === 'stdio') {
|
136
|
+
if (!mcp.command) throw new Error(t('dev.mcp.command.required'));
|
137
|
+
if (!mcp.args) throw new Error(t('dev.mcp.args.required'));
|
138
|
+
data = await mcpService.getStdioMcpServerManifest(id, mcp.command, mcp.args);
|
139
|
+
} else {
|
140
|
+
throw new Error('Invalid MCP type'); // Internal error
|
141
|
+
}
|
142
|
+
|
143
|
+
setManifest(data);
|
144
|
+
// Optionally update form if manifest ID differs or to store the fetched manifest
|
145
|
+
// Be careful about overwriting user input if not desired
|
146
|
+
form.setFieldsValue({ manifest: data });
|
147
|
+
} catch (error) {
|
148
|
+
// Check if error is a validation error object (from validateFields)
|
149
|
+
|
150
|
+
// Handle API call errors or other errors
|
151
|
+
const err = error as Error; // Assuming PluginInstallError or similar structure
|
152
|
+
// Use the error message directly if it's a simple string error, otherwise try translation
|
153
|
+
// highlight-start
|
154
|
+
const errorMessage = t('error.testConnectionFailed', {
|
155
|
+
error: err.cause || err.message || t('unknownError'),
|
156
|
+
});
|
157
|
+
// highlight-end
|
158
|
+
|
159
|
+
setConnectionError(errorMessage);
|
160
|
+
} finally {
|
161
|
+
setIsTesting(false);
|
162
|
+
}
|
163
|
+
};
|
55
164
|
|
56
|
-
const HTTP_URL_KEY = ['customParams', 'mcp', 'url'];
|
57
|
-
const STDIO_COMMAND = ['customParams', 'mcp', 'command'];
|
58
|
-
const STDIO_ARGS = ['customParams', 'mcp', 'args'];
|
59
165
|
return (
|
60
166
|
<Form form={form} layout={'vertical'}>
|
61
167
|
<Flexbox>
|
@@ -66,12 +172,16 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
66
172
|
>
|
67
173
|
<MCPTypeSelect />
|
68
174
|
</Form.Item>
|
175
|
+
{/* 仅在有粘贴相关错误时显示 Alert */}
|
176
|
+
{pasteError && (
|
177
|
+
<Alert message={pasteError} showIcon style={{ marginBottom: 16 }} type="error" />
|
178
|
+
)}
|
69
179
|
<Form.Item
|
70
180
|
extra={t('dev.mcp.identifier.desc')}
|
71
181
|
label={t('dev.mcp.identifier.label')}
|
72
182
|
name={'identifier'}
|
73
183
|
rules={[
|
74
|
-
{ required: true },
|
184
|
+
{ message: t('dev.mcp.identifier.required'), required: true },
|
75
185
|
{
|
76
186
|
message: t('dev.mcp.identifier.invalid'),
|
77
187
|
pattern: /^[\w-]+$/,
|
@@ -90,63 +200,23 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
90
200
|
},
|
91
201
|
]}
|
92
202
|
>
|
93
|
-
<Input
|
203
|
+
<Input
|
204
|
+
onChange={handleIdentifierChange}
|
205
|
+
placeholder={t('dev.mcp.identifier.placeholder')}
|
206
|
+
/>
|
94
207
|
</Form.Item>
|
95
208
|
|
96
209
|
{mcpType === 'http' && (
|
97
210
|
<Form.Item
|
98
|
-
extra={
|
99
|
-
<Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
|
100
|
-
{t('dev.mcp.url.desc')}
|
101
|
-
{manifest && (
|
102
|
-
<ManifestPreviewer manifest={manifest}>
|
103
|
-
<ActionIcon
|
104
|
-
icon={FileCode}
|
105
|
-
size={'small'}
|
106
|
-
title={t('dev.meta.manifest.preview')}
|
107
|
-
/>
|
108
|
-
</ManifestPreviewer>
|
109
|
-
)}
|
110
|
-
</Flexbox>
|
111
|
-
}
|
112
|
-
hasFeedback
|
211
|
+
extra={t('dev.mcp.url.desc')}
|
113
212
|
label={t('dev.mcp.url.label')}
|
114
213
|
name={HTTP_URL_KEY}
|
115
214
|
rules={[
|
116
|
-
{ required: true },
|
117
|
-
{ type: 'url' },
|
118
|
-
{
|
119
|
-
validator: async (_, value) => {
|
120
|
-
if (!value) return true;
|
121
|
-
try {
|
122
|
-
const data = await mcpService.getStreamableMcpServerManifest(
|
123
|
-
form.getFieldValue('identifier'),
|
124
|
-
value,
|
125
|
-
);
|
126
|
-
setManifest(data);
|
127
|
-
form.setFieldsValue({ identifier: data.identifier, manifest: data });
|
128
|
-
} catch (error) {
|
129
|
-
const err = error as PluginInstallError;
|
130
|
-
throw t(`error.${err.message}`, { error: err.cause! });
|
131
|
-
}
|
132
|
-
},
|
133
|
-
},
|
215
|
+
{ message: t('dev.mcp.url.required'), required: true },
|
216
|
+
{ message: t('dev.mcp.url.invalid'), type: 'url' },
|
134
217
|
]}
|
135
218
|
>
|
136
|
-
<Input
|
137
|
-
placeholder="https://mcp.higress.ai/mcp-github/xxxxx"
|
138
|
-
suffix={
|
139
|
-
<ActionIcon
|
140
|
-
icon={RotateCwIcon}
|
141
|
-
onClick={(e) => {
|
142
|
-
e.stopPropagation();
|
143
|
-
form.validateFields([HTTP_URL_KEY]);
|
144
|
-
}}
|
145
|
-
size={'small'}
|
146
|
-
title={t('dev.meta.manifest.refresh')}
|
147
|
-
/>
|
148
|
-
}
|
149
|
-
/>
|
219
|
+
<Input placeholder="https://mcp.higress.ai/mcp-github/xxxxx" />
|
150
220
|
</Form.Item>
|
151
221
|
)}
|
152
222
|
|
@@ -156,7 +226,7 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
156
226
|
extra={t('dev.mcp.command.desc')}
|
157
227
|
label={t('dev.mcp.command.label')}
|
158
228
|
name={STDIO_COMMAND}
|
159
|
-
rules={[{ required: true }]}
|
229
|
+
rules={[{ message: t('dev.mcp.command.required'), required: true }]}
|
160
230
|
>
|
161
231
|
<AutoComplete
|
162
232
|
options={STDIO_COMMAND_OPTIONS.map(({ value, icon: Icon, color }) => ({
|
@@ -173,50 +243,43 @@ const MCPManifestForm = ({ form, isEditMode }: MCPManifestFormProps) => {
|
|
173
243
|
</Form.Item>
|
174
244
|
<Form.Item
|
175
245
|
extra={t('dev.mcp.args.desc')}
|
176
|
-
hasFeedback
|
177
246
|
label={t('dev.mcp.args.label')}
|
178
247
|
name={STDIO_ARGS}
|
179
|
-
rules={[
|
180
|
-
{ required: true },
|
181
|
-
{
|
182
|
-
validator: async (_, value) => {
|
183
|
-
if (!value) return true;
|
184
|
-
const name = form.getFieldValue('identifier');
|
185
|
-
|
186
|
-
if (!name) throw new Error('Please input mcp server name');
|
187
|
-
try {
|
188
|
-
const data = await mcpService.getStdioMcpServerManifest(
|
189
|
-
name,
|
190
|
-
form.getFieldValue(STDIO_COMMAND),
|
191
|
-
value,
|
192
|
-
);
|
193
|
-
setManifest(data);
|
194
|
-
form.setFieldsValue({ identifier: data.identifier, manifest: data });
|
195
|
-
} catch (error) {
|
196
|
-
const err = error as PluginInstallError;
|
197
|
-
throw t(`error.${err.message}`, { error: err.cause! });
|
198
|
-
}
|
199
|
-
},
|
200
|
-
},
|
201
|
-
]}
|
248
|
+
rules={[{ message: t('dev.mcp.args.required'), required: true }]}
|
202
249
|
>
|
203
|
-
<ArgsInput
|
204
|
-
placeholder={t('dev.mcp.args.placeholder')}
|
205
|
-
suffix={
|
206
|
-
<ActionIcon
|
207
|
-
icon={RotateCwIcon}
|
208
|
-
onClick={(e) => {
|
209
|
-
e.stopPropagation();
|
210
|
-
form.validateFields([STDIO_ARGS]);
|
211
|
-
}}
|
212
|
-
size={'small'}
|
213
|
-
title={t('dev.meta.manifest.refresh')}
|
214
|
-
/>
|
215
|
-
}
|
216
|
-
/>
|
250
|
+
<ArgsInput placeholder={t('dev.mcp.args.placeholder')} />
|
217
251
|
</Form.Item>
|
218
252
|
</>
|
219
253
|
)}
|
254
|
+
<Form.Item extra={t('dev.mcp.testConnectionTip')}>
|
255
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
256
|
+
<Button
|
257
|
+
loading={isTesting}
|
258
|
+
onClick={handleTestConnection}
|
259
|
+
type={!!mcpType ? 'primary' : undefined}
|
260
|
+
>
|
261
|
+
{t('dev.mcp.testConnection')}
|
262
|
+
</Button>
|
263
|
+
{manifest && !connectionError && !isTesting && (
|
264
|
+
<ManifestPreviewer manifest={manifest}>
|
265
|
+
<Flexbox>
|
266
|
+
<Button icon={<Icon icon={FileCode} />}>{t('dev.mcp.previewManifest')}</Button>
|
267
|
+
</Flexbox>
|
268
|
+
</ManifestPreviewer>
|
269
|
+
)}
|
270
|
+
</Flexbox>
|
271
|
+
</Form.Item>
|
272
|
+
|
273
|
+
{connectionError && (
|
274
|
+
<Alert
|
275
|
+
closable
|
276
|
+
message={connectionError}
|
277
|
+
onClose={() => setConnectionError(null)}
|
278
|
+
showIcon
|
279
|
+
style={{ marginBottom: 16 }}
|
280
|
+
type="error"
|
281
|
+
/>
|
282
|
+
)}
|
220
283
|
<FormItem name={'manifest'} noStyle />
|
221
284
|
</Flexbox>
|
222
285
|
</Form>
|
@@ -0,0 +1,262 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import { McpParseErrorCode, parseMcpInput } from './utils';
|
4
|
+
|
5
|
+
describe('parseMcpInput', () => {
|
6
|
+
// Test Suite 1: Valid Nested mcpServers Structure
|
7
|
+
describe('Nested mcpServers Structure', () => {
|
8
|
+
it('should correctly parse valid stdio config', () => {
|
9
|
+
const input = JSON.stringify({
|
10
|
+
mcpServers: {
|
11
|
+
'sequential-thinking': {
|
12
|
+
command: 'npx',
|
13
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
14
|
+
},
|
15
|
+
},
|
16
|
+
});
|
17
|
+
const expected = {
|
18
|
+
status: 'success',
|
19
|
+
identifier: 'sequential-thinking',
|
20
|
+
mcpConfig: {
|
21
|
+
command: 'npx',
|
22
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
23
|
+
type: 'stdio',
|
24
|
+
},
|
25
|
+
};
|
26
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
27
|
+
});
|
28
|
+
|
29
|
+
it('should correctly parse valid http config', () => {
|
30
|
+
const input = JSON.stringify({
|
31
|
+
mcpServers: {
|
32
|
+
'some-http-service': {
|
33
|
+
url: 'https://example.com/api',
|
34
|
+
},
|
35
|
+
},
|
36
|
+
});
|
37
|
+
const expected = {
|
38
|
+
status: 'success',
|
39
|
+
identifier: 'some-http-service',
|
40
|
+
mcpConfig: {
|
41
|
+
url: 'https://example.com/api',
|
42
|
+
type: 'http',
|
43
|
+
},
|
44
|
+
};
|
45
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
46
|
+
});
|
47
|
+
|
48
|
+
it('should correctly parse valid http config with empty string identifier', () => {
|
49
|
+
const input = JSON.stringify({
|
50
|
+
mcpServers: {
|
51
|
+
'': {
|
52
|
+
url: 'https://router.mcp.so/mcp/mdvp27m9tl2bxs',
|
53
|
+
},
|
54
|
+
},
|
55
|
+
});
|
56
|
+
const expected = {
|
57
|
+
status: 'success',
|
58
|
+
identifier: '',
|
59
|
+
mcpConfig: {
|
60
|
+
url: 'https://router.mcp.so/mcp/mdvp27m9tl2bxs',
|
61
|
+
type: 'http',
|
62
|
+
},
|
63
|
+
};
|
64
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
65
|
+
});
|
66
|
+
|
67
|
+
it('should return error for empty mcpServers object', () => {
|
68
|
+
const input = JSON.stringify({ mcpServers: {} });
|
69
|
+
const expected = {
|
70
|
+
status: 'error',
|
71
|
+
errorCode: McpParseErrorCode.EmptyMcpServers,
|
72
|
+
};
|
73
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
74
|
+
});
|
75
|
+
|
76
|
+
it('should return error for invalid structure within mcpServers config', () => {
|
77
|
+
const input = JSON.stringify({
|
78
|
+
mcpServers: {
|
79
|
+
'invalid-config': {}, // Missing command/args or url
|
80
|
+
},
|
81
|
+
});
|
82
|
+
const expected = {
|
83
|
+
status: 'error',
|
84
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
85
|
+
identifier: 'invalid-config',
|
86
|
+
};
|
87
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
88
|
+
});
|
89
|
+
|
90
|
+
it('should return error if mcpConfig is not an object', () => {
|
91
|
+
const input = JSON.stringify({
|
92
|
+
mcpServers: {
|
93
|
+
'not-an-object': 'hello',
|
94
|
+
},
|
95
|
+
});
|
96
|
+
const expected = {
|
97
|
+
status: 'error',
|
98
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
99
|
+
identifier: 'not-an-object',
|
100
|
+
};
|
101
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
102
|
+
});
|
103
|
+
|
104
|
+
it('should return error if mcpConfig is null', () => {
|
105
|
+
const input = JSON.stringify({
|
106
|
+
mcpServers: {
|
107
|
+
'is-null': null,
|
108
|
+
},
|
109
|
+
});
|
110
|
+
const expected = {
|
111
|
+
status: 'error',
|
112
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
113
|
+
identifier: 'is-null',
|
114
|
+
};
|
115
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
116
|
+
});
|
117
|
+
});
|
118
|
+
|
119
|
+
// Test Suite 2: Valid Flat Structure (Top-level Identifier)
|
120
|
+
describe('Flat Structure (Top-level Identifier)', () => {
|
121
|
+
it('should correctly parse valid stdio config', () => {
|
122
|
+
const input = JSON.stringify({
|
123
|
+
'flat-stdio-service': {
|
124
|
+
command: 'go',
|
125
|
+
args: ['run', 'main.go'],
|
126
|
+
},
|
127
|
+
});
|
128
|
+
const expected = {
|
129
|
+
status: 'success',
|
130
|
+
identifier: 'flat-stdio-service',
|
131
|
+
mcpConfig: {
|
132
|
+
command: 'go',
|
133
|
+
args: ['run', 'main.go'],
|
134
|
+
type: 'stdio',
|
135
|
+
},
|
136
|
+
};
|
137
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
138
|
+
});
|
139
|
+
|
140
|
+
it('should correctly parse valid http config', () => {
|
141
|
+
const input = JSON.stringify({
|
142
|
+
'mcp-wolframalpha': {
|
143
|
+
url: 'https://mcp.higress.ai/mcp-wolframalpha/abc',
|
144
|
+
},
|
145
|
+
});
|
146
|
+
const expected = {
|
147
|
+
status: 'success',
|
148
|
+
identifier: 'mcp-wolframalpha',
|
149
|
+
mcpConfig: {
|
150
|
+
url: 'https://mcp.higress.ai/mcp-wolframalpha/abc',
|
151
|
+
type: 'http',
|
152
|
+
},
|
153
|
+
};
|
154
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
155
|
+
});
|
156
|
+
|
157
|
+
it('should return error for invalid structure within flat config', () => {
|
158
|
+
const input = JSON.stringify({
|
159
|
+
'invalid-flat': {}, // Missing command/args or url
|
160
|
+
});
|
161
|
+
const expected = {
|
162
|
+
status: 'error',
|
163
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
164
|
+
identifier: 'invalid-flat',
|
165
|
+
};
|
166
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
167
|
+
});
|
168
|
+
|
169
|
+
it('should return error if the value associated with the identifier is not an object', () => {
|
170
|
+
const input = JSON.stringify({
|
171
|
+
'flat-not-object': 'just a string',
|
172
|
+
});
|
173
|
+
const expected = {
|
174
|
+
status: 'error',
|
175
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
176
|
+
identifier: 'flat-not-object',
|
177
|
+
};
|
178
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
179
|
+
});
|
180
|
+
|
181
|
+
it('should return error if the value associated with the identifier is null', () => {
|
182
|
+
const input = JSON.stringify({
|
183
|
+
'flat-is-null': null,
|
184
|
+
});
|
185
|
+
const expected = {
|
186
|
+
status: 'error',
|
187
|
+
errorCode: McpParseErrorCode.InvalidMcpStructure,
|
188
|
+
identifier: 'flat-is-null',
|
189
|
+
};
|
190
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
191
|
+
});
|
192
|
+
|
193
|
+
it('should return error for multiple top-level keys', () => {
|
194
|
+
const input = JSON.stringify({
|
195
|
+
key1: { url: 'url1' },
|
196
|
+
key2: { url: 'url2' },
|
197
|
+
});
|
198
|
+
const expected = {
|
199
|
+
status: 'error',
|
200
|
+
errorCode: McpParseErrorCode.InvalidJsonStructure, // Because it's not a single-key flat structure nor mcpServers/manifest
|
201
|
+
};
|
202
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
203
|
+
});
|
204
|
+
});
|
205
|
+
|
206
|
+
// Test Suite 4: Invalid Inputs and Edge Cases
|
207
|
+
describe('Invalid Inputs and Edge Cases', () => {
|
208
|
+
it('should return noop for invalid JSON string', () => {
|
209
|
+
const input = 'this is not json';
|
210
|
+
const expected = { status: 'noop' };
|
211
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
212
|
+
});
|
213
|
+
|
214
|
+
it('should return noop for empty string', () => {
|
215
|
+
const input = '';
|
216
|
+
const expected = { status: 'noop' };
|
217
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
218
|
+
});
|
219
|
+
|
220
|
+
it('should return noop for null input', () => {
|
221
|
+
// @ts-ignore testing invalid input type
|
222
|
+
const input = null;
|
223
|
+
const expected = { status: 'noop' };
|
224
|
+
expect(parseMcpInput(input as any)).toEqual(expected);
|
225
|
+
});
|
226
|
+
|
227
|
+
it('should return noop for undefined input', () => {
|
228
|
+
// @ts-ignore testing invalid input type
|
229
|
+
const input = undefined;
|
230
|
+
const expected = { status: 'noop' };
|
231
|
+
expect(parseMcpInput(input as any)).toEqual(expected);
|
232
|
+
});
|
233
|
+
|
234
|
+
it('should return InvalidJsonStructure for empty JSON object', () => {
|
235
|
+
const input = JSON.stringify({});
|
236
|
+
// Empty object is considered an invalid structure because it doesn't match any expected format
|
237
|
+
const expected = {
|
238
|
+
status: 'error',
|
239
|
+
errorCode: McpParseErrorCode.InvalidJsonStructure,
|
240
|
+
};
|
241
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
242
|
+
});
|
243
|
+
|
244
|
+
it('should return noop for JSON array', () => {
|
245
|
+
const input = JSON.stringify([]);
|
246
|
+
const expected = { status: 'noop' };
|
247
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
248
|
+
});
|
249
|
+
|
250
|
+
it('should return noop for JSON primitive (string)', () => {
|
251
|
+
const input = JSON.stringify('just a string');
|
252
|
+
const expected = { status: 'noop' };
|
253
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
254
|
+
});
|
255
|
+
|
256
|
+
it('should return noop for JSON primitive (number)', () => {
|
257
|
+
const input = JSON.stringify(123);
|
258
|
+
const expected = { status: 'noop' };
|
259
|
+
expect(parseMcpInput(input)).toEqual(expected);
|
260
|
+
});
|
261
|
+
});
|
262
|
+
});
|