@svadmin/mcp 0.1.0

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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@svadmin/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP (Model Context Protocol) server — expose svadmin DataProvider as tools for AI agents",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "src/**/*"
9
+ ],
10
+ "main": "src/index.ts",
11
+ "types": "src/index.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ }
17
+ },
18
+ "peerDependencies": {
19
+ "@svadmin/core": "^0.5.0"
20
+ },
21
+ "license": "MIT",
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "author": "zuohuadong",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/zuohuadong/svadmin.git",
30
+ "directory": "packages/mcp"
31
+ },
32
+ "homepage": "https://github.com/zuohuadong/svadmin#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/zuohuadong/svadmin/issues"
35
+ }
36
+ }
@@ -0,0 +1,250 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { createMCPServer } from './index';
3
+ import type { MCPRequest } from './index';
4
+
5
+ // ─── Mock DataProvider ───────────────────────────────────────
6
+
7
+ interface MockFilter { field: string; operator: string; value: unknown }
8
+ interface MockPagination { current?: number; pageSize?: number }
9
+
10
+ const mockData: Record<string, Record<string, unknown>[]> = {
11
+ posts: [
12
+ { id: '1', title: 'Hello World', status: 'published' },
13
+ { id: '2', title: 'Draft Post', status: 'draft' },
14
+ { id: '3', title: 'Another Post', status: 'published' },
15
+ ],
16
+ users: [
17
+ { id: '1', name: 'Alice', email: 'alice@test.com' },
18
+ { id: '2', name: 'Bob', email: 'bob@test.com' },
19
+ ],
20
+ };
21
+
22
+ const mockDataProvider = {
23
+ getList: async (params: { resource: string; pagination?: MockPagination; filters?: MockFilter[] }) => {
24
+ let data = [...(mockData[params.resource] ?? [])];
25
+ if (params.filters && Array.isArray(params.filters)) {
26
+ for (const f of params.filters) {
27
+ data = data.filter(item => item[f.field] === f.value);
28
+ }
29
+ }
30
+ const page = params.pagination?.current ?? 1;
31
+ const size = params.pagination?.pageSize ?? 10;
32
+ const start = (page - 1) * size;
33
+ return { data: data.slice(start, start + size), total: data.length };
34
+ },
35
+ getOne: async (params: { resource: string; id: string | number }) => {
36
+ const item = (mockData[params.resource] ?? []).find(r => r.id === params.id);
37
+ if (!item) throw new Error(`Not found: ${params.resource}/${params.id}`);
38
+ return { data: item };
39
+ },
40
+ getMany: async (params: { resource: string; ids: (string | number)[] }) => {
41
+ const items = (mockData[params.resource] ?? []).filter(r => params.ids.includes(r.id as string));
42
+ return { data: items };
43
+ },
44
+ create: async (params: { resource: string; variables: unknown }) => {
45
+ const newItem = { id: String(Date.now()), ...(params.variables as Record<string, unknown>) };
46
+ return { data: newItem };
47
+ },
48
+ update: async (params: { resource: string; id: string | number; variables: unknown }) => {
49
+ const existing = (mockData[params.resource] ?? []).find(r => r.id === params.id);
50
+ const updated = { ...existing, ...(params.variables as Record<string, unknown>) };
51
+ return { data: updated };
52
+ },
53
+ deleteOne: async (params: { resource: string; id: string | number }) => {
54
+ const item = (mockData[params.resource] ?? []).find(r => r.id === params.id);
55
+ return { data: item ?? { id: params.id } };
56
+ },
57
+ getApiUrl: () => 'http://localhost:3000/api',
58
+ };
59
+
60
+ // ─── Helper ──────────────────────────────────────────────────
61
+
62
+ function makeRequest(method: string, params?: Record<string, unknown>): MCPRequest {
63
+ return { jsonrpc: '2.0', id: 1, method, params };
64
+ }
65
+
66
+ // ─── Tests ───────────────────────────────────────────────────
67
+
68
+ describe('createMCPServer', () => {
69
+ const server = createMCPServer({
70
+ dataProvider: mockDataProvider as Parameters<typeof createMCPServer>[0]['dataProvider'],
71
+ resources: ['posts', 'users'],
72
+ });
73
+
74
+ test('generates correct number of tools (read + write)', () => {
75
+ const tools = server.getTools();
76
+ // getList, getOne, getMany, create, update, delete = 6
77
+ expect(tools).toHaveLength(6);
78
+ expect(tools.map(t => t.name)).toEqual([
79
+ 'svadmin_getList', 'svadmin_getOne', 'svadmin_getMany',
80
+ 'svadmin_create', 'svadmin_update', 'svadmin_delete',
81
+ ]);
82
+ });
83
+
84
+ test('readOnly mode excludes write tools', () => {
85
+ const readOnlyServer = createMCPServer({
86
+ dataProvider: mockDataProvider as Parameters<typeof createMCPServer>[0]['dataProvider'],
87
+ resources: ['posts'],
88
+ readOnly: true,
89
+ });
90
+ const tools = readOnlyServer.getTools();
91
+ expect(tools).toHaveLength(3);
92
+ expect(tools.map(t => t.name)).toEqual([
93
+ 'svadmin_getList', 'svadmin_getOne', 'svadmin_getMany',
94
+ ]);
95
+ });
96
+
97
+ test('tool resource enum matches configured resources', () => {
98
+ const tools = server.getTools();
99
+ const getListTool = tools.find(t => t.name === 'svadmin_getList')!;
100
+ expect(getListTool.inputSchema.properties.resource.enum).toEqual(['posts', 'users']);
101
+ });
102
+
103
+ test('getServerInfo returns correct metadata', () => {
104
+ const info = server.getServerInfo();
105
+ expect(info.name).toBe('@svadmin/mcp');
106
+ expect(info.version).toBe('0.1.0');
107
+ expect(info.tools).toHaveLength(6);
108
+ });
109
+ });
110
+
111
+ describe('MCP JSON-RPC handling', () => {
112
+ const server = createMCPServer({
113
+ dataProvider: mockDataProvider as Parameters<typeof createMCPServer>[0]['dataProvider'],
114
+ resources: ['posts', 'users'],
115
+ });
116
+
117
+ test('initialize handshake', async () => {
118
+ const res = await server.handleRequest(makeRequest('initialize'));
119
+ expect(res.error).toBeUndefined();
120
+ const result = res.result as Record<string, unknown>;
121
+ expect(result.protocolVersion).toBe('2024-11-05');
122
+ expect((result.serverInfo as Record<string, unknown>).name).toBe('@svadmin/mcp');
123
+ });
124
+
125
+ test('tools/list returns all tools', async () => {
126
+ const res = await server.handleRequest(makeRequest('tools/list'));
127
+ expect(res.error).toBeUndefined();
128
+ const result = res.result as { tools: unknown[] };
129
+ expect(result.tools).toHaveLength(6);
130
+ });
131
+
132
+ test('tools/call getList returns paginated data', async () => {
133
+ const res = await server.handleRequest(makeRequest('tools/call', {
134
+ name: 'svadmin_getList',
135
+ arguments: { resource: 'posts', page: 1, pageSize: 2 },
136
+ }));
137
+
138
+ expect(res.error).toBeUndefined();
139
+ const result = res.result as { content: { type: string; text: string }[] };
140
+ const parsed = JSON.parse(result.content[0].text);
141
+ expect(parsed.data).toHaveLength(2);
142
+ expect(parsed.total).toBe(3);
143
+ });
144
+
145
+ test('tools/call getList with filters', async () => {
146
+ const res = await server.handleRequest(makeRequest('tools/call', {
147
+ name: 'svadmin_getList',
148
+ arguments: {
149
+ resource: 'posts',
150
+ filters: JSON.stringify([{ field: 'status', operator: 'eq', value: 'draft' }]),
151
+ },
152
+ }));
153
+
154
+ expect(res.error).toBeUndefined();
155
+ const result = res.result as { content: { type: string; text: string }[] };
156
+ const parsed = JSON.parse(result.content[0].text);
157
+ expect(parsed.data).toHaveLength(1);
158
+ expect(parsed.data[0].title).toBe('Draft Post');
159
+ });
160
+
161
+ test('tools/call getOne returns single record', async () => {
162
+ const res = await server.handleRequest(makeRequest('tools/call', {
163
+ name: 'svadmin_getOne',
164
+ arguments: { resource: 'users', id: '1' },
165
+ }));
166
+
167
+ expect(res.error).toBeUndefined();
168
+ const result = res.result as { content: { type: string; text: string }[] };
169
+ const parsed = JSON.parse(result.content[0].text);
170
+ expect(parsed.data.name).toBe('Alice');
171
+ });
172
+
173
+ test('tools/call getMany returns multiple records', async () => {
174
+ const res = await server.handleRequest(makeRequest('tools/call', {
175
+ name: 'svadmin_getMany',
176
+ arguments: { resource: 'users', ids: '1,2' },
177
+ }));
178
+
179
+ expect(res.error).toBeUndefined();
180
+ const result = res.result as { content: { type: string; text: string }[] };
181
+ const parsed = JSON.parse(result.content[0].text);
182
+ expect(parsed.data).toHaveLength(2);
183
+ });
184
+
185
+ test('tools/call create returns new record', async () => {
186
+ const res = await server.handleRequest(makeRequest('tools/call', {
187
+ name: 'svadmin_create',
188
+ arguments: {
189
+ resource: 'posts',
190
+ data: JSON.stringify({ title: 'New Post', status: 'draft' }),
191
+ },
192
+ }));
193
+
194
+ expect(res.error).toBeUndefined();
195
+ const result = res.result as { content: { type: string; text: string }[] };
196
+ const parsed = JSON.parse(result.content[0].text);
197
+ expect(parsed.data.title).toBe('New Post');
198
+ expect(parsed.data.id).toBeDefined();
199
+ });
200
+
201
+ test('tools/call update returns modified record', async () => {
202
+ const res = await server.handleRequest(makeRequest('tools/call', {
203
+ name: 'svadmin_update',
204
+ arguments: {
205
+ resource: 'posts',
206
+ id: '1',
207
+ data: JSON.stringify({ title: 'Updated Title' }),
208
+ },
209
+ }));
210
+
211
+ expect(res.error).toBeUndefined();
212
+ const result = res.result as { content: { type: string; text: string }[] };
213
+ const parsed = JSON.parse(result.content[0].text);
214
+ expect(parsed.data.title).toBe('Updated Title');
215
+ });
216
+
217
+ test('tools/call delete returns deleted record', async () => {
218
+ const res = await server.handleRequest(makeRequest('tools/call', {
219
+ name: 'svadmin_delete',
220
+ arguments: { resource: 'posts', id: '2' },
221
+ }));
222
+
223
+ expect(res.error).toBeUndefined();
224
+ const result = res.result as { content: { type: string; text: string }[] };
225
+ const parsed = JSON.parse(result.content[0].text);
226
+ expect(parsed.data.id).toBe('2');
227
+ });
228
+
229
+ test('unknown tool returns error', async () => {
230
+ const res = await server.handleRequest(makeRequest('tools/call', {
231
+ name: 'svadmin_nope',
232
+ arguments: {},
233
+ }));
234
+
235
+ expect(res.error).toBeDefined();
236
+ expect(res.error!.code).toBe(-32602);
237
+ });
238
+
239
+ test('unknown method returns error', async () => {
240
+ const res = await server.handleRequest(makeRequest('whatever'));
241
+ expect(res.error).toBeDefined();
242
+ expect(res.error!.code).toBe(-32601);
243
+ });
244
+
245
+ test('missing tool name returns error', async () => {
246
+ const res = await server.handleRequest(makeRequest('tools/call', {}));
247
+ expect(res.error).toBeDefined();
248
+ expect(res.error!.code).toBe(-32602);
249
+ });
250
+ });
package/src/index.ts ADDED
@@ -0,0 +1,376 @@
1
+ /**
2
+ * @svadmin/mcp — Model Context Protocol server for svadmin DataProvider
3
+ *
4
+ * Exposes svadmin's DataProvider CRUD operations as MCP-compatible tools,
5
+ * allowing any MCP client (Claude Desktop, Cursor, custom agents) to
6
+ * query and mutate admin data through a standardized protocol.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createMCPServer } from '@svadmin/mcp';
11
+ * import { myDataProvider } from './data-provider';
12
+ *
13
+ * const server = createMCPServer({
14
+ * dataProvider: myDataProvider,
15
+ * resources: ['posts', 'users', 'orders'],
16
+ * });
17
+ *
18
+ * // Start stdio transport (for Claude Desktop / Cursor)
19
+ * server.listen();
20
+ *
21
+ * // Or get the handler for HTTP transport
22
+ * const handler = server.createHandler();
23
+ * ```
24
+ */
25
+
26
+ import type { DataProvider, ResourceDefinition } from '@svadmin/core';
27
+
28
+ // ─── Types ───────────────────────────────────────────────────
29
+
30
+ export interface MCPServerOptions {
31
+ /** The svadmin DataProvider to expose via MCP */
32
+ dataProvider: DataProvider;
33
+ /** List of resource names to expose (whitelist) */
34
+ resources: string[];
35
+ /** Optional resource definitions for richer tool descriptions */
36
+ resourceDefinitions?: ResourceDefinition[];
37
+ /** Server name shown to MCP clients */
38
+ name?: string;
39
+ /** Server version */
40
+ version?: string;
41
+ /** When true, only expose read operations (getList, getOne, getMany) */
42
+ readOnly?: boolean;
43
+ }
44
+
45
+ /** MCP Tool definition following the Model Context Protocol spec */
46
+ export interface MCPTool {
47
+ name: string;
48
+ description: string;
49
+ inputSchema: {
50
+ type: 'object';
51
+ properties: Record<string, MCPPropertySchema>;
52
+ required?: string[];
53
+ };
54
+ }
55
+
56
+ export interface MCPPropertySchema {
57
+ type: string;
58
+ description?: string;
59
+ enum?: string[];
60
+ items?: MCPPropertySchema;
61
+ default?: unknown;
62
+ }
63
+
64
+ /** MCP JSON-RPC request */
65
+ export interface MCPRequest {
66
+ jsonrpc: '2.0';
67
+ id: string | number;
68
+ method: string;
69
+ params?: Record<string, unknown>;
70
+ }
71
+
72
+ /** MCP JSON-RPC response */
73
+ export interface MCPResponse {
74
+ jsonrpc: '2.0';
75
+ id: string | number;
76
+ result?: unknown;
77
+ error?: { code: number; message: string; data?: unknown };
78
+ }
79
+
80
+ /** MCP Server instance */
81
+ export interface MCPServer {
82
+ /** Get all tool definitions */
83
+ getTools(): MCPTool[];
84
+ /** Handle a single MCP JSON-RPC request */
85
+ handleRequest(request: MCPRequest): Promise<MCPResponse>;
86
+ /** Get the server info for MCP handshake */
87
+ getServerInfo(): { name: string; version: string; tools: MCPTool[] };
88
+ }
89
+
90
+ // ─── Tool Generation ─────────────────────────────────────────
91
+
92
+ function buildResourceParam(resources: string[]): MCPPropertySchema {
93
+ return {
94
+ type: 'string',
95
+ description: 'Resource name to operate on',
96
+ enum: resources,
97
+ };
98
+ }
99
+
100
+ function generateTools(options: MCPServerOptions): MCPTool[] {
101
+ const { resources, readOnly = false } = options;
102
+ const tools: MCPTool[] = [];
103
+
104
+ // getList
105
+ tools.push({
106
+ name: 'svadmin_getList',
107
+ description: 'Fetch a paginated list of records from a resource. Supports filtering and sorting.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ resource: buildResourceParam(resources),
112
+ page: { type: 'number', description: 'Page number (1-indexed)', default: 1 },
113
+ pageSize: { type: 'number', description: 'Number of records per page', default: 10 },
114
+ sortField: { type: 'string', description: 'Field to sort by' },
115
+ sortOrder: { type: 'string', description: 'Sort direction', enum: ['asc', 'desc'] },
116
+ filters: { type: 'string', description: 'JSON-encoded filter array: [{"field":"status","operator":"eq","value":"active"}]' },
117
+ },
118
+ required: ['resource'],
119
+ },
120
+ });
121
+
122
+ // getOne
123
+ tools.push({
124
+ name: 'svadmin_getOne',
125
+ description: 'Fetch a single record by ID from a resource.',
126
+ inputSchema: {
127
+ type: 'object',
128
+ properties: {
129
+ resource: buildResourceParam(resources),
130
+ id: { type: 'string', description: 'Record ID' },
131
+ },
132
+ required: ['resource', 'id'],
133
+ },
134
+ });
135
+
136
+ // getMany
137
+ tools.push({
138
+ name: 'svadmin_getMany',
139
+ description: 'Fetch multiple records by their IDs from a resource.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ resource: buildResourceParam(resources),
144
+ ids: { type: 'string', description: 'Comma-separated list of record IDs' },
145
+ },
146
+ required: ['resource', 'ids'],
147
+ },
148
+ });
149
+
150
+ if (!readOnly) {
151
+ // create
152
+ tools.push({
153
+ name: 'svadmin_create',
154
+ description: 'Create a new record in a resource.',
155
+ inputSchema: {
156
+ type: 'object',
157
+ properties: {
158
+ resource: buildResourceParam(resources),
159
+ data: { type: 'string', description: 'JSON-encoded record data to create' },
160
+ },
161
+ required: ['resource', 'data'],
162
+ },
163
+ });
164
+
165
+ // update
166
+ tools.push({
167
+ name: 'svadmin_update',
168
+ description: 'Update an existing record by ID in a resource.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: {
172
+ resource: buildResourceParam(resources),
173
+ id: { type: 'string', description: 'Record ID to update' },
174
+ data: { type: 'string', description: 'JSON-encoded partial record data' },
175
+ },
176
+ required: ['resource', 'id', 'data'],
177
+ },
178
+ });
179
+
180
+ // delete
181
+ tools.push({
182
+ name: 'svadmin_delete',
183
+ description: 'Delete a record by ID from a resource.',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ resource: buildResourceParam(resources),
188
+ id: { type: 'string', description: 'Record ID to delete' },
189
+ },
190
+ required: ['resource', 'id'],
191
+ },
192
+ });
193
+ }
194
+
195
+ return tools;
196
+ }
197
+
198
+ // ─── Request Handler ─────────────────────────────────────────
199
+
200
+ async function handleToolCall(
201
+ toolName: string,
202
+ args: Record<string, unknown>,
203
+ dp: DataProvider,
204
+ ): Promise<unknown> {
205
+ switch (toolName) {
206
+ case 'svadmin_getList': {
207
+ const resource = args.resource as string;
208
+ const page = (args.page as number) ?? 1;
209
+ const pageSize = (args.pageSize as number) ?? 10;
210
+ const sorters = args.sortField
211
+ ? [{ field: args.sortField as string, order: (args.sortOrder as 'asc' | 'desc') ?? 'asc' }]
212
+ : undefined;
213
+ const filters = args.filters
214
+ ? JSON.parse(args.filters as string)
215
+ : undefined;
216
+
217
+ return dp.getList({
218
+ resource,
219
+ pagination: { current: page, pageSize },
220
+ sorters,
221
+ filters,
222
+ });
223
+ }
224
+
225
+ case 'svadmin_getOne': {
226
+ return dp.getOne({
227
+ resource: args.resource as string,
228
+ id: args.id as string,
229
+ });
230
+ }
231
+
232
+ case 'svadmin_getMany': {
233
+ const ids = (args.ids as string).split(',').map(id => id.trim());
234
+ if (dp.getMany) {
235
+ return dp.getMany({ resource: args.resource as string, ids });
236
+ }
237
+ // Fallback: fetch one by one
238
+ const results = await Promise.all(
239
+ ids.map(id => dp.getOne({ resource: args.resource as string, id }))
240
+ );
241
+ return { data: results.map(r => r.data) };
242
+ }
243
+
244
+ case 'svadmin_create': {
245
+ const data = JSON.parse(args.data as string);
246
+ return dp.create({
247
+ resource: args.resource as string,
248
+ variables: data,
249
+ });
250
+ }
251
+
252
+ case 'svadmin_update': {
253
+ const data = JSON.parse(args.data as string);
254
+ return dp.update({
255
+ resource: args.resource as string,
256
+ id: args.id as string,
257
+ variables: data,
258
+ });
259
+ }
260
+
261
+ case 'svadmin_delete': {
262
+ return dp.deleteOne({
263
+ resource: args.resource as string,
264
+ id: args.id as string,
265
+ });
266
+ }
267
+
268
+ default:
269
+ throw new Error(`Unknown tool: ${toolName}`);
270
+ }
271
+ }
272
+
273
+ // ─── Server Factory ──────────────────────────────────────────
274
+
275
+ /**
276
+ * Create an MCP server that exposes svadmin DataProvider operations as tools.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * const server = createMCPServer({
281
+ * dataProvider: myDataProvider,
282
+ * resources: ['posts', 'users'],
283
+ * readOnly: false,
284
+ * });
285
+ *
286
+ * // Handle a JSON-RPC request
287
+ * const response = await server.handleRequest({
288
+ * jsonrpc: '2.0',
289
+ * id: 1,
290
+ * method: 'tools/call',
291
+ * params: { name: 'svadmin_getList', arguments: { resource: 'posts', page: 1 } },
292
+ * });
293
+ * ```
294
+ */
295
+ export function createMCPServer(options: MCPServerOptions): MCPServer {
296
+ const serverName = options.name ?? '@svadmin/mcp';
297
+ const serverVersion = options.version ?? '0.1.0';
298
+ const tools = generateTools(options);
299
+
300
+ async function handleRequest(request: MCPRequest): Promise<MCPResponse> {
301
+ const { id, method, params } = request;
302
+
303
+ try {
304
+ switch (method) {
305
+ case 'initialize':
306
+ return {
307
+ jsonrpc: '2.0',
308
+ id,
309
+ result: {
310
+ protocolVersion: '2024-11-05',
311
+ serverInfo: { name: serverName, version: serverVersion },
312
+ capabilities: { tools: {} },
313
+ },
314
+ };
315
+
316
+ case 'tools/list':
317
+ return {
318
+ jsonrpc: '2.0',
319
+ id,
320
+ result: { tools },
321
+ };
322
+
323
+ case 'tools/call': {
324
+ const toolName = (params as Record<string, unknown>)?.name as string;
325
+ const toolArgs = ((params as Record<string, unknown>)?.arguments ?? {}) as Record<string, unknown>;
326
+
327
+ if (!toolName) {
328
+ return {
329
+ jsonrpc: '2.0',
330
+ id,
331
+ error: { code: -32602, message: 'Missing tool name' },
332
+ };
333
+ }
334
+
335
+ const knownTool = tools.find(t => t.name === toolName);
336
+ if (!knownTool) {
337
+ return {
338
+ jsonrpc: '2.0',
339
+ id,
340
+ error: { code: -32602, message: `Unknown tool: ${toolName}` },
341
+ };
342
+ }
343
+
344
+ const result = await handleToolCall(toolName, toolArgs, options.dataProvider);
345
+ return {
346
+ jsonrpc: '2.0',
347
+ id,
348
+ result: {
349
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
350
+ },
351
+ };
352
+ }
353
+
354
+ default:
355
+ return {
356
+ jsonrpc: '2.0',
357
+ id,
358
+ error: { code: -32601, message: `Method not found: ${method}` },
359
+ };
360
+ }
361
+ } catch (err: unknown) {
362
+ const message = err instanceof Error ? err.message : String(err);
363
+ return {
364
+ jsonrpc: '2.0',
365
+ id,
366
+ error: { code: -32000, message },
367
+ };
368
+ }
369
+ }
370
+
371
+ return {
372
+ getTools: () => tools,
373
+ getServerInfo: () => ({ name: serverName, version: serverVersion, tools }),
374
+ handleRequest,
375
+ };
376
+ }