@ottocode/server 0.1.260 → 0.1.261

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.
Files changed (67) hide show
  1. package/package.json +4 -3
  2. package/src/index.ts +5 -4
  3. package/src/openapi/register.ts +92 -0
  4. package/src/openapi/route.ts +22 -0
  5. package/src/routes/ask.ts +210 -99
  6. package/src/routes/auth.ts +1701 -626
  7. package/src/routes/branch.ts +281 -90
  8. package/src/routes/config/agents.ts +79 -32
  9. package/src/routes/config/cwd.ts +46 -14
  10. package/src/routes/config/debug.ts +159 -30
  11. package/src/routes/config/defaults.ts +182 -64
  12. package/src/routes/config/main.ts +109 -73
  13. package/src/routes/config/models.ts +304 -137
  14. package/src/routes/config/providers.ts +462 -166
  15. package/src/routes/config/utils.ts +2 -2
  16. package/src/routes/doctor.ts +395 -161
  17. package/src/routes/files.ts +650 -260
  18. package/src/routes/git/branch.ts +143 -52
  19. package/src/routes/git/commit.ts +347 -141
  20. package/src/routes/git/diff.ts +239 -116
  21. package/src/routes/git/init.ts +103 -23
  22. package/src/routes/git/pull.ts +167 -65
  23. package/src/routes/git/push.ts +222 -117
  24. package/src/routes/git/remote.ts +401 -100
  25. package/src/routes/git/staging.ts +502 -141
  26. package/src/routes/git/status.ts +171 -78
  27. package/src/routes/mcp.ts +1129 -404
  28. package/src/routes/openapi.ts +27 -4
  29. package/src/routes/ottorouter.ts +1221 -389
  30. package/src/routes/provider-usage.ts +153 -36
  31. package/src/routes/research.ts +817 -370
  32. package/src/routes/root.ts +50 -6
  33. package/src/routes/session-approval.ts +228 -54
  34. package/src/routes/session-files.ts +265 -134
  35. package/src/routes/session-messages.ts +330 -150
  36. package/src/routes/session-stream.ts +83 -2
  37. package/src/routes/sessions.ts +1830 -780
  38. package/src/routes/skills.ts +849 -161
  39. package/src/routes/terminals.ts +469 -103
  40. package/src/routes/tunnel.ts +394 -118
  41. package/src/runtime/ask/service.ts +1 -0
  42. package/src/runtime/message/compaction-limits.ts +3 -3
  43. package/src/runtime/provider/reasoning.ts +2 -1
  44. package/src/runtime/session/db-operations.ts +4 -3
  45. package/src/runtime/utils/token.ts +7 -2
  46. package/src/tools/adapter.ts +21 -0
  47. package/src/openapi/paths/ask.ts +0 -81
  48. package/src/openapi/paths/auth.ts +0 -687
  49. package/src/openapi/paths/branch.ts +0 -102
  50. package/src/openapi/paths/config.ts +0 -485
  51. package/src/openapi/paths/doctor.ts +0 -165
  52. package/src/openapi/paths/files.ts +0 -236
  53. package/src/openapi/paths/git.ts +0 -690
  54. package/src/openapi/paths/mcp.ts +0 -339
  55. package/src/openapi/paths/messages.ts +0 -103
  56. package/src/openapi/paths/ottorouter.ts +0 -594
  57. package/src/openapi/paths/provider-usage.ts +0 -59
  58. package/src/openapi/paths/research.ts +0 -227
  59. package/src/openapi/paths/session-approval.ts +0 -93
  60. package/src/openapi/paths/session-extras.ts +0 -336
  61. package/src/openapi/paths/session-files.ts +0 -91
  62. package/src/openapi/paths/sessions.ts +0 -210
  63. package/src/openapi/paths/skills.ts +0 -377
  64. package/src/openapi/paths/stream.ts +0 -26
  65. package/src/openapi/paths/terminals.ts +0 -226
  66. package/src/openapi/paths/tunnel.ts +0 -163
  67. package/src/openapi/spec.ts +0 -73
package/src/routes/mcp.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  removeMCPServerFromConfig,
15
15
  } from '@ottocode/sdk';
16
16
  import { authorizeCopilot, pollForCopilotTokenOnce } from '@ottocode/sdk';
17
+ import { openApiRoute } from '../openapi/route.ts';
17
18
 
18
19
  const copilotMCPOAuthStore = new OAuthCredentialStore();
19
20
 
@@ -28,155 +29,678 @@ const copilotMCPSessions = new Map<
28
29
  >();
29
30
 
30
31
  export function registerMCPRoutes(app: Hono) {
31
- app.get('/v1/mcp/servers', async (c) => {
32
- const projectRoot = process.cwd();
33
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
34
- const manager = getMCPManager();
35
- const statuses = manager ? await manager.getStatusAsync() : [];
36
-
37
- const servers = config.servers.map((s) => {
38
- const status = statuses.find((st) => st.name === s.name);
39
- return {
40
- name: s.name,
41
- transport: s.transport ?? 'stdio',
42
- command: s.command,
43
- args: s.args ?? [],
44
- url: s.url,
45
- disabled: s.disabled ?? false,
46
- connected: status?.connected ?? false,
47
- tools: status?.tools ?? [],
48
- authRequired: status?.authRequired ?? false,
49
- authenticated: status?.authenticated ?? false,
50
- scope: s.scope ?? 'global',
51
- ...(isGitHubCopilotUrl(s.url) ? { authType: 'copilot-device' } : {}),
32
+ openApiRoute(
33
+ app,
34
+ {
35
+ method: 'get',
36
+ path: '/v1/mcp/servers',
37
+ tags: ['mcp'],
38
+ operationId: 'listMCPServers',
39
+ summary: 'List configured MCP servers',
40
+ responses: {
41
+ '200': {
42
+ description: 'OK',
43
+ content: {
44
+ 'application/json': {
45
+ schema: {
46
+ type: 'object',
47
+ properties: {
48
+ servers: {
49
+ type: 'array',
50
+ items: {
51
+ $ref: '#/components/schemas/MCPServer',
52
+ },
53
+ },
54
+ },
55
+ required: ['servers'],
56
+ },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ },
62
+ async (c) => {
63
+ const projectRoot = process.cwd();
64
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
65
+ const manager = getMCPManager();
66
+ const statuses = manager ? await manager.getStatusAsync() : [];
67
+
68
+ const servers = config.servers.map((s) => {
69
+ const status = statuses.find((st) => st.name === s.name);
70
+ return {
71
+ name: s.name,
72
+ transport: s.transport ?? 'stdio',
73
+ command: s.command,
74
+ args: s.args ?? [],
75
+ url: s.url,
76
+ disabled: s.disabled ?? false,
77
+ connected: status?.connected ?? false,
78
+ tools: status?.tools ?? [],
79
+ authRequired: status?.authRequired ?? false,
80
+ authenticated: status?.authenticated ?? false,
81
+ scope: s.scope ?? 'global',
82
+ ...(isGitHubCopilotUrl(s.url) ? { authType: 'copilot-device' } : {}),
83
+ };
84
+ });
85
+
86
+ return c.json({ servers });
87
+ },
88
+ );
89
+
90
+ openApiRoute(
91
+ app,
92
+ {
93
+ method: 'post',
94
+ path: '/v1/mcp/servers',
95
+ tags: ['mcp'],
96
+ operationId: 'addMCPServer',
97
+ summary: 'Add a new MCP server',
98
+ requestBody: {
99
+ required: true,
100
+ content: {
101
+ 'application/json': {
102
+ schema: {
103
+ type: 'object',
104
+ properties: {
105
+ name: {
106
+ type: 'string',
107
+ },
108
+ transport: {
109
+ type: 'string',
110
+ enum: ['stdio', 'http', 'sse'],
111
+ default: 'stdio',
112
+ },
113
+ command: {
114
+ type: 'string',
115
+ },
116
+ args: {
117
+ type: 'array',
118
+ items: {
119
+ type: 'string',
120
+ },
121
+ },
122
+ env: {
123
+ type: 'object',
124
+ additionalProperties: {
125
+ type: 'string',
126
+ },
127
+ },
128
+ url: {
129
+ type: 'string',
130
+ },
131
+ headers: {
132
+ type: 'object',
133
+ additionalProperties: {
134
+ type: 'string',
135
+ },
136
+ },
137
+ oauth: {
138
+ type: 'object',
139
+ },
140
+ scope: {
141
+ type: 'string',
142
+ enum: ['global', 'project'],
143
+ default: 'global',
144
+ },
145
+ },
146
+ required: ['name'],
147
+ },
148
+ },
149
+ },
150
+ },
151
+ responses: {
152
+ '200': {
153
+ description: 'OK',
154
+ content: {
155
+ 'application/json': {
156
+ schema: {
157
+ type: 'object',
158
+ properties: {
159
+ ok: {
160
+ type: 'boolean',
161
+ },
162
+ error: {
163
+ type: 'string',
164
+ },
165
+ },
166
+ required: ['ok'],
167
+ },
168
+ },
169
+ },
170
+ },
171
+ '400': {
172
+ description: 'Bad Request',
173
+ content: {
174
+ 'application/json': {
175
+ schema: {
176
+ type: 'object',
177
+ properties: {
178
+ error: {
179
+ type: 'string',
180
+ },
181
+ },
182
+ required: ['error'],
183
+ },
184
+ },
185
+ },
186
+ },
187
+ },
188
+ },
189
+ async (c) => {
190
+ const projectRoot = process.cwd();
191
+ const body = await c.req.json();
192
+
193
+ const {
194
+ name,
195
+ transport,
196
+ command,
197
+ args,
198
+ env,
199
+ url,
200
+ headers,
201
+ oauth,
202
+ scope,
203
+ } = body;
204
+ if (!name) {
205
+ return c.json({ ok: false, error: 'name is required' }, 400);
206
+ }
207
+
208
+ const t = transport ?? 'stdio';
209
+ if (t === 'stdio' && !command) {
210
+ return c.json(
211
+ { ok: false, error: 'command is required for stdio transport' },
212
+ 400,
213
+ );
214
+ }
215
+ if (t === 'stdio' && command && /^https?:\/\//i.test(String(command))) {
216
+ return c.json(
217
+ {
218
+ ok: false,
219
+ error:
220
+ 'stdio transport requires a local command, not a URL. Use http or sse transport for remote servers.',
221
+ },
222
+ 400,
223
+ );
224
+ }
225
+ if ((t === 'http' || t === 'sse') && !url) {
226
+ return c.json(
227
+ { ok: false, error: 'url is required for http/sse transport' },
228
+ 400,
229
+ );
230
+ }
231
+
232
+ const serverScope = scope === 'project' ? 'project' : 'global';
233
+
234
+ const serverConfig = {
235
+ name: String(name),
236
+ transport: t,
237
+ scope: serverScope as 'global' | 'project',
238
+ ...(command ? { command: String(command) } : {}),
239
+ ...(Array.isArray(args) ? { args: args.map(String) } : {}),
240
+ ...(env && typeof env === 'object' ? { env } : {}),
241
+ ...(url ? { url: String(url) } : {}),
242
+ ...(headers && typeof headers === 'object' ? { headers } : {}),
243
+ ...(oauth && typeof oauth === 'object' ? { oauth } : {}),
52
244
  };
53
- });
54
-
55
- return c.json({ servers });
56
- });
57
-
58
- app.post('/v1/mcp/servers', async (c) => {
59
- const projectRoot = process.cwd();
60
- const body = await c.req.json();
61
-
62
- const { name, transport, command, args, env, url, headers, oauth, scope } =
63
- body;
64
- if (!name) {
65
- return c.json({ ok: false, error: 'name is required' }, 400);
66
- }
67
-
68
- const t = transport ?? 'stdio';
69
- if (t === 'stdio' && !command) {
70
- return c.json(
71
- { ok: false, error: 'command is required for stdio transport' },
72
- 400,
73
- );
74
- }
75
- if (t === 'stdio' && command && /^https?:\/\//i.test(String(command))) {
76
- return c.json(
245
+
246
+ try {
247
+ await addMCPServerToConfig(
248
+ projectRoot,
249
+ serverConfig,
250
+ getGlobalConfigDir(),
251
+ );
252
+ return c.json({ ok: true, server: serverConfig });
253
+ } catch (err) {
254
+ const msg = err instanceof Error ? err.message : String(err);
255
+ return c.json({ ok: false, error: msg }, 500);
256
+ }
257
+ },
258
+ );
259
+
260
+ openApiRoute(
261
+ app,
262
+ {
263
+ method: 'delete',
264
+ path: '/v1/mcp/servers/{name}',
265
+ tags: ['mcp'],
266
+ operationId: 'removeMCPServer',
267
+ summary: 'Remove an MCP server',
268
+ parameters: [
77
269
  {
78
- ok: false,
79
- error:
80
- 'stdio transport requires a local command, not a URL. Use http or sse transport for remote servers.',
270
+ in: 'path',
271
+ name: 'name',
272
+ required: true,
273
+ schema: {
274
+ type: 'string',
275
+ },
276
+ description: 'MCP server name',
81
277
  },
82
- 400,
83
- );
84
- }
85
- if ((t === 'http' || t === 'sse') && !url) {
86
- return c.json(
87
- { ok: false, error: 'url is required for http/sse transport' },
88
- 400,
89
- );
90
- }
91
-
92
- const serverScope = scope === 'project' ? 'project' : 'global';
93
-
94
- const serverConfig = {
95
- name: String(name),
96
- transport: t,
97
- scope: serverScope as 'global' | 'project',
98
- ...(command ? { command: String(command) } : {}),
99
- ...(Array.isArray(args) ? { args: args.map(String) } : {}),
100
- ...(env && typeof env === 'object' ? { env } : {}),
101
- ...(url ? { url: String(url) } : {}),
102
- ...(headers && typeof headers === 'object' ? { headers } : {}),
103
- ...(oauth && typeof oauth === 'object' ? { oauth } : {}),
104
- };
105
-
106
- try {
107
- await addMCPServerToConfig(
108
- projectRoot,
109
- serverConfig,
110
- getGlobalConfigDir(),
111
- );
112
- return c.json({ ok: true, server: serverConfig });
113
- } catch (err) {
114
- const msg = err instanceof Error ? err.message : String(err);
115
- return c.json({ ok: false, error: msg }, 500);
116
- }
117
- });
118
-
119
- app.delete('/v1/mcp/servers/:name', async (c) => {
120
- const name = c.req.param('name');
121
- const projectRoot = process.cwd();
122
-
123
- try {
124
- const manager = getMCPManager();
125
- if (manager) {
126
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
127
- const serverConfig = config.servers.find((s) => s.name === name);
128
- const scope = serverConfig?.scope ?? 'global';
129
- await manager.clearAuthData(name, scope, projectRoot);
130
- await manager.stopServer(name);
278
+ ],
279
+ responses: {
280
+ '200': {
281
+ description: 'OK',
282
+ content: {
283
+ 'application/json': {
284
+ schema: {
285
+ type: 'object',
286
+ properties: {
287
+ ok: {
288
+ type: 'boolean',
289
+ },
290
+ error: {
291
+ type: 'string',
292
+ },
293
+ },
294
+ required: ['ok'],
295
+ },
296
+ },
297
+ },
298
+ },
299
+ '404': {
300
+ description: 'Bad Request',
301
+ content: {
302
+ 'application/json': {
303
+ schema: {
304
+ type: 'object',
305
+ properties: {
306
+ error: {
307
+ type: 'string',
308
+ },
309
+ },
310
+ required: ['error'],
311
+ },
312
+ },
313
+ },
314
+ },
315
+ },
316
+ },
317
+ async (c) => {
318
+ const name = c.req.param('name');
319
+ const projectRoot = process.cwd();
320
+
321
+ try {
322
+ const manager = getMCPManager();
323
+ if (manager) {
324
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
325
+ const serverConfig = config.servers.find((s) => s.name === name);
326
+ const scope = serverConfig?.scope ?? 'global';
327
+ await manager.clearAuthData(name, scope, projectRoot);
328
+ await manager.stopServer(name);
329
+ }
330
+
331
+ const removed = await removeMCPServerFromConfig(
332
+ projectRoot,
333
+ name,
334
+ getGlobalConfigDir(),
335
+ );
336
+ if (!removed) {
337
+ return c.json(
338
+ { ok: false, error: `Server "${name}" not found` },
339
+ 404,
340
+ );
341
+ }
342
+ return c.json({ ok: true, name });
343
+ } catch (err) {
344
+ const msg = err instanceof Error ? err.message : String(err);
345
+ return c.json({ ok: false, error: msg }, 500);
131
346
  }
347
+ },
348
+ );
132
349
 
133
- const removed = await removeMCPServerFromConfig(
134
- projectRoot,
135
- name,
136
- getGlobalConfigDir(),
137
- );
138
- if (!removed) {
350
+ openApiRoute(
351
+ app,
352
+ {
353
+ method: 'post',
354
+ path: '/v1/mcp/servers/{name}/start',
355
+ tags: ['mcp'],
356
+ operationId: 'startMCPServer',
357
+ summary: 'Start an MCP server',
358
+ parameters: [
359
+ {
360
+ in: 'path',
361
+ name: 'name',
362
+ required: true,
363
+ schema: {
364
+ type: 'string',
365
+ },
366
+ description: 'MCP server name',
367
+ },
368
+ ],
369
+ responses: {
370
+ '200': {
371
+ description: 'OK',
372
+ content: {
373
+ 'application/json': {
374
+ schema: {
375
+ type: 'object',
376
+ properties: {
377
+ ok: {
378
+ type: 'boolean',
379
+ },
380
+ name: {
381
+ type: 'string',
382
+ },
383
+ connected: {
384
+ type: 'boolean',
385
+ },
386
+ tools: {
387
+ type: 'array',
388
+ items: {
389
+ type: 'object',
390
+ properties: {
391
+ name: {
392
+ type: 'string',
393
+ },
394
+ description: {
395
+ type: 'string',
396
+ },
397
+ },
398
+ },
399
+ },
400
+ authRequired: {
401
+ type: 'boolean',
402
+ },
403
+ authType: {
404
+ type: 'string',
405
+ },
406
+ sessionId: {
407
+ type: 'string',
408
+ },
409
+ userCode: {
410
+ type: 'string',
411
+ },
412
+ verificationUri: {
413
+ type: 'string',
414
+ },
415
+ interval: {
416
+ type: 'integer',
417
+ },
418
+ authUrl: {
419
+ type: 'string',
420
+ },
421
+ error: {
422
+ type: 'string',
423
+ },
424
+ },
425
+ required: ['ok'],
426
+ },
427
+ },
428
+ },
429
+ },
430
+ '404': {
431
+ description: 'Bad Request',
432
+ content: {
433
+ 'application/json': {
434
+ schema: {
435
+ type: 'object',
436
+ properties: {
437
+ error: {
438
+ type: 'string',
439
+ },
440
+ },
441
+ required: ['error'],
442
+ },
443
+ },
444
+ },
445
+ },
446
+ },
447
+ },
448
+ async (c) => {
449
+ const name = c.req.param('name');
450
+ const projectRoot = process.cwd();
451
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
452
+ const serverConfig = config.servers.find((s) => s.name === name);
453
+
454
+ if (!serverConfig) {
139
455
  return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
140
456
  }
141
- return c.json({ ok: true, name });
142
- } catch (err) {
143
- const msg = err instanceof Error ? err.message : String(err);
144
- return c.json({ ok: false, error: msg }, 500);
145
- }
146
- });
147
-
148
- app.post('/v1/mcp/servers/:name/start', async (c) => {
149
- const name = c.req.param('name');
150
- const projectRoot = process.cwd();
151
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
152
- const serverConfig = config.servers.find((s) => s.name === name);
153
-
154
- if (!serverConfig) {
155
- return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
156
- }
157
-
158
- try {
159
- let manager = getMCPManager();
457
+
458
+ try {
459
+ let manager = getMCPManager();
460
+ if (!manager) {
461
+ manager = await initializeMCP({ servers: [] }, projectRoot);
462
+ }
463
+ if (!manager.started) {
464
+ manager.setProjectRoot(projectRoot);
465
+ }
466
+ await manager.restartServer(serverConfig);
467
+ const status = (await manager.getStatusAsync()).find(
468
+ (s) => s.name === name,
469
+ );
470
+
471
+ if (isGitHubCopilotUrl(serverConfig.url) && !status?.connected) {
472
+ const existingAuth = await getStoredCopilotMCPToken(
473
+ copilotMCPOAuthStore,
474
+ name,
475
+ serverConfig.scope ?? 'global',
476
+ projectRoot,
477
+ );
478
+
479
+ if (!existingAuth.token || existingAuth.needsReauth) {
480
+ const deviceData = await authorizeCopilot({ mcp: true });
481
+ const sessionId = crypto.randomUUID();
482
+ copilotMCPSessions.set(sessionId, {
483
+ deviceCode: deviceData.deviceCode,
484
+ interval: deviceData.interval,
485
+ serverName: name,
486
+ createdAt: Date.now(),
487
+ });
488
+ return c.json({
489
+ ok: true,
490
+ name,
491
+ connected: false,
492
+ authRequired: true,
493
+ authType: 'copilot-device',
494
+ sessionId,
495
+ userCode: deviceData.userCode,
496
+ verificationUri: deviceData.verificationUri,
497
+ interval: deviceData.interval,
498
+ });
499
+ }
500
+ }
501
+
502
+ return c.json({
503
+ ok: true,
504
+ name,
505
+ connected: status?.connected ?? false,
506
+ tools: status?.tools ?? [],
507
+ authRequired: status?.authRequired ?? false,
508
+ authUrl: manager.getAuthUrl(name),
509
+ });
510
+ } catch (err) {
511
+ const msg = err instanceof Error ? err.message : String(err);
512
+ return c.json({ ok: false, error: msg }, 500);
513
+ }
514
+ },
515
+ );
516
+
517
+ openApiRoute(
518
+ app,
519
+ {
520
+ method: 'post',
521
+ path: '/v1/mcp/servers/{name}/stop',
522
+ tags: ['mcp'],
523
+ operationId: 'stopMCPServer',
524
+ summary: 'Stop an MCP server',
525
+ parameters: [
526
+ {
527
+ in: 'path',
528
+ name: 'name',
529
+ required: true,
530
+ schema: {
531
+ type: 'string',
532
+ },
533
+ description: 'MCP server name',
534
+ },
535
+ ],
536
+ responses: {
537
+ '200': {
538
+ description: 'OK',
539
+ content: {
540
+ 'application/json': {
541
+ schema: {
542
+ type: 'object',
543
+ properties: {
544
+ ok: {
545
+ type: 'boolean',
546
+ },
547
+ error: {
548
+ type: 'string',
549
+ },
550
+ },
551
+ required: ['ok'],
552
+ },
553
+ },
554
+ },
555
+ },
556
+ '400': {
557
+ description: 'Bad Request',
558
+ content: {
559
+ 'application/json': {
560
+ schema: {
561
+ type: 'object',
562
+ properties: {
563
+ error: {
564
+ type: 'string',
565
+ },
566
+ },
567
+ required: ['error'],
568
+ },
569
+ },
570
+ },
571
+ },
572
+ },
573
+ },
574
+ async (c) => {
575
+ const name = c.req.param('name');
576
+ const manager = getMCPManager();
577
+
160
578
  if (!manager) {
161
- manager = await initializeMCP({ servers: [] }, projectRoot);
579
+ return c.json({ ok: false, error: 'No MCP manager active' }, 400);
162
580
  }
163
- if (!manager.started) {
164
- manager.setProjectRoot(projectRoot);
581
+
582
+ try {
583
+ await manager.stopServer(name);
584
+ return c.json({ ok: true, name, connected: false });
585
+ } catch (err) {
586
+ const msg = err instanceof Error ? err.message : String(err);
587
+ return c.json({ ok: false, error: msg }, 500);
588
+ }
589
+ },
590
+ );
591
+
592
+ openApiRoute(
593
+ app,
594
+ {
595
+ method: 'post',
596
+ path: '/v1/mcp/servers/{name}/auth',
597
+ tags: ['mcp'],
598
+ operationId: 'initiateMCPAuth',
599
+ summary: 'Initiate auth for an MCP server',
600
+ parameters: [
601
+ {
602
+ in: 'path',
603
+ name: 'name',
604
+ required: true,
605
+ schema: {
606
+ type: 'string',
607
+ },
608
+ description: 'MCP server name',
609
+ },
610
+ ],
611
+ responses: {
612
+ '200': {
613
+ description: 'OK',
614
+ content: {
615
+ 'application/json': {
616
+ schema: {
617
+ type: 'object',
618
+ properties: {
619
+ ok: {
620
+ type: 'boolean',
621
+ },
622
+ name: {
623
+ type: 'string',
624
+ },
625
+ authUrl: {
626
+ type: 'string',
627
+ },
628
+ authType: {
629
+ type: 'string',
630
+ },
631
+ authenticated: {
632
+ type: 'boolean',
633
+ },
634
+ sessionId: {
635
+ type: 'string',
636
+ },
637
+ userCode: {
638
+ type: 'string',
639
+ },
640
+ verificationUri: {
641
+ type: 'string',
642
+ },
643
+ interval: {
644
+ type: 'integer',
645
+ },
646
+ message: {
647
+ type: 'string',
648
+ },
649
+ error: {
650
+ type: 'string',
651
+ },
652
+ },
653
+ required: ['ok'],
654
+ },
655
+ },
656
+ },
657
+ },
658
+ '404': {
659
+ description: 'Bad Request',
660
+ content: {
661
+ 'application/json': {
662
+ schema: {
663
+ type: 'object',
664
+ properties: {
665
+ error: {
666
+ type: 'string',
667
+ },
668
+ },
669
+ required: ['error'],
670
+ },
671
+ },
672
+ },
673
+ },
674
+ },
675
+ },
676
+ async (c) => {
677
+ const name = c.req.param('name');
678
+ const projectRoot = process.cwd();
679
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
680
+ const serverConfig = config.servers.find((s) => s.name === name);
681
+
682
+ if (!serverConfig) {
683
+ return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
165
684
  }
166
- await manager.restartServer(serverConfig);
167
- const status = (await manager.getStatusAsync()).find(
168
- (s) => s.name === name,
169
- );
170
-
171
- if (isGitHubCopilotUrl(serverConfig.url) && !status?.connected) {
172
- const existingAuth = await getStoredCopilotMCPToken(
173
- copilotMCPOAuthStore,
174
- name,
175
- serverConfig.scope ?? 'global',
176
- projectRoot,
177
- );
178
685
 
179
- if (!existingAuth.token || existingAuth.needsReauth) {
686
+ if (isGitHubCopilotUrl(serverConfig.url)) {
687
+ try {
688
+ const existingAuth = await getStoredCopilotMCPToken(
689
+ copilotMCPOAuthStore,
690
+ name,
691
+ serverConfig.scope ?? 'global',
692
+ projectRoot,
693
+ );
694
+ if (existingAuth.token && !existingAuth.needsReauth) {
695
+ return c.json({
696
+ ok: true,
697
+ name,
698
+ authType: 'copilot-device',
699
+ authenticated: true,
700
+ message: 'Already authenticated with MCP scopes',
701
+ });
702
+ }
703
+
180
704
  const deviceData = await authorizeCopilot({ mcp: true });
181
705
  const sessionId = crypto.randomUUID();
182
706
  copilotMCPSessions.set(sessionId, {
@@ -188,325 +712,526 @@ export function registerMCPRoutes(app: Hono) {
188
712
  return c.json({
189
713
  ok: true,
190
714
  name,
191
- connected: false,
192
- authRequired: true,
193
715
  authType: 'copilot-device',
194
716
  sessionId,
195
717
  userCode: deviceData.userCode,
196
718
  verificationUri: deviceData.verificationUri,
197
719
  interval: deviceData.interval,
198
720
  });
721
+ } catch (err) {
722
+ const msg = err instanceof Error ? err.message : String(err);
723
+ return c.json({ ok: false, error: msg }, 500);
199
724
  }
200
725
  }
201
726
 
202
- return c.json({
203
- ok: true,
204
- name,
205
- connected: status?.connected ?? false,
206
- tools: status?.tools ?? [],
207
- authRequired: status?.authRequired ?? false,
208
- authUrl: manager.getAuthUrl(name),
209
- });
210
- } catch (err) {
211
- const msg = err instanceof Error ? err.message : String(err);
212
- return c.json({ ok: false, error: msg }, 500);
213
- }
214
- });
215
-
216
- app.post('/v1/mcp/servers/:name/stop', async (c) => {
217
- const name = c.req.param('name');
218
- const manager = getMCPManager();
219
-
220
- if (!manager) {
221
- return c.json({ ok: false, error: 'No MCP manager active' }, 400);
222
- }
223
-
224
- try {
225
- await manager.stopServer(name);
226
- return c.json({ ok: true, name, connected: false });
227
- } catch (err) {
228
- const msg = err instanceof Error ? err.message : String(err);
229
- return c.json({ ok: false, error: msg }, 500);
230
- }
231
- });
232
-
233
- app.post('/v1/mcp/servers/:name/auth', async (c) => {
234
- const name = c.req.param('name');
235
- const projectRoot = process.cwd();
236
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
237
- const serverConfig = config.servers.find((s) => s.name === name);
238
-
239
- if (!serverConfig) {
240
- return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
241
- }
242
-
243
- if (isGitHubCopilotUrl(serverConfig.url)) {
244
727
  try {
245
- const existingAuth = await getStoredCopilotMCPToken(
246
- copilotMCPOAuthStore,
247
- name,
248
- serverConfig.scope ?? 'global',
249
- projectRoot,
250
- );
251
- if (existingAuth.token && !existingAuth.needsReauth) {
252
- return c.json({
253
- ok: true,
254
- name,
255
- authType: 'copilot-device',
256
- authenticated: true,
257
- message: 'Already authenticated with MCP scopes',
258
- });
728
+ let manager = getMCPManager();
729
+ if (!manager) {
730
+ manager = await initializeMCP({ servers: [] }, projectRoot);
731
+ }
732
+ if (!manager.started) {
733
+ manager.setProjectRoot(projectRoot);
259
734
  }
260
735
 
261
- const deviceData = await authorizeCopilot({ mcp: true });
262
- const sessionId = crypto.randomUUID();
263
- copilotMCPSessions.set(sessionId, {
264
- deviceCode: deviceData.deviceCode,
265
- interval: deviceData.interval,
266
- serverName: name,
267
- createdAt: Date.now(),
268
- });
736
+ const authUrl = await manager.initiateAuth(serverConfig);
737
+ if (authUrl) {
738
+ return c.json({ ok: true, authUrl, name });
739
+ }
269
740
  return c.json({
270
741
  ok: true,
271
742
  name,
272
- authType: 'copilot-device',
273
- sessionId,
274
- userCode: deviceData.userCode,
275
- verificationUri: deviceData.verificationUri,
276
- interval: deviceData.interval,
743
+ message: 'Already authenticated or no auth required',
277
744
  });
278
745
  } catch (err) {
279
746
  const msg = err instanceof Error ? err.message : String(err);
280
747
  return c.json({ ok: false, error: msg }, 500);
281
748
  }
282
- }
749
+ },
750
+ );
283
751
 
284
- try {
285
- let manager = getMCPManager();
286
- if (!manager) {
287
- manager = await initializeMCP({ servers: [] }, projectRoot);
288
- }
289
- if (!manager.started) {
290
- manager.setProjectRoot(projectRoot);
752
+ openApiRoute(
753
+ app,
754
+ {
755
+ method: 'post',
756
+ path: '/v1/mcp/servers/{name}/auth/callback',
757
+ tags: ['mcp'],
758
+ operationId: 'completeMCPAuth',
759
+ summary: 'Complete MCP server auth callback',
760
+ parameters: [
761
+ {
762
+ in: 'path',
763
+ name: 'name',
764
+ required: true,
765
+ schema: {
766
+ type: 'string',
767
+ },
768
+ description: 'MCP server name',
769
+ },
770
+ ],
771
+ requestBody: {
772
+ required: true,
773
+ content: {
774
+ 'application/json': {
775
+ schema: {
776
+ type: 'object',
777
+ properties: {
778
+ code: {
779
+ type: 'string',
780
+ },
781
+ sessionId: {
782
+ type: 'string',
783
+ },
784
+ },
785
+ },
786
+ },
787
+ },
788
+ },
789
+ responses: {
790
+ '200': {
791
+ description: 'OK',
792
+ content: {
793
+ 'application/json': {
794
+ schema: {
795
+ type: 'object',
796
+ properties: {
797
+ ok: {
798
+ type: 'boolean',
799
+ },
800
+ status: {
801
+ type: 'string',
802
+ enum: ['complete', 'pending', 'error'],
803
+ },
804
+ name: {
805
+ type: 'string',
806
+ },
807
+ connected: {
808
+ type: 'boolean',
809
+ },
810
+ tools: {
811
+ type: 'array',
812
+ items: {
813
+ type: 'object',
814
+ properties: {
815
+ name: {
816
+ type: 'string',
817
+ },
818
+ description: {
819
+ type: 'string',
820
+ },
821
+ },
822
+ },
823
+ },
824
+ error: {
825
+ type: 'string',
826
+ },
827
+ },
828
+ required: ['ok'],
829
+ },
830
+ },
831
+ },
832
+ },
833
+ '400': {
834
+ description: 'Bad Request',
835
+ content: {
836
+ 'application/json': {
837
+ schema: {
838
+ type: 'object',
839
+ properties: {
840
+ error: {
841
+ type: 'string',
842
+ },
843
+ },
844
+ required: ['error'],
845
+ },
846
+ },
847
+ },
848
+ },
849
+ },
850
+ },
851
+ async (c) => {
852
+ const name = c.req.param('name');
853
+ const body = await c.req.json();
854
+ const { code, sessionId } = body;
855
+
856
+ if (sessionId) {
857
+ const session = copilotMCPSessions.get(sessionId);
858
+ if (!session || session.serverName !== name) {
859
+ return c.json(
860
+ { ok: false, error: 'Session expired or invalid' },
861
+ 400,
862
+ );
863
+ }
864
+ try {
865
+ const result = await pollForCopilotTokenOnce(session.deviceCode);
866
+ if (result.status === 'complete') {
867
+ copilotMCPSessions.delete(sessionId);
868
+ const projectRoot = process.cwd();
869
+ const config = await loadMCPConfig(
870
+ projectRoot,
871
+ getGlobalConfigDir(),
872
+ );
873
+ const serverConfig = config.servers.find((s) => s.name === name);
874
+ if (!serverConfig) {
875
+ return c.json(
876
+ { ok: false, error: `Server "${name}" not found` },
877
+ 404,
878
+ );
879
+ }
880
+ await copilotMCPOAuthStore.saveTokens(
881
+ getCopilotMCPOAuthKey(
882
+ name,
883
+ serverConfig.scope ?? 'global',
884
+ projectRoot,
885
+ ),
886
+ {
887
+ access_token: result.accessToken,
888
+ scope: COPILOT_MCP_SCOPE,
889
+ },
890
+ );
891
+ let mcpMgr = getMCPManager();
892
+ if (!mcpMgr) {
893
+ mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
894
+ }
895
+ await mcpMgr.restartServer(serverConfig);
896
+ mcpMgr = getMCPManager();
897
+ const status = mcpMgr
898
+ ? (await mcpMgr.getStatusAsync()).find((s) => s.name === name)
899
+ : undefined;
900
+ return c.json({
901
+ ok: true,
902
+ status: 'complete',
903
+ name,
904
+ connected: status?.connected ?? false,
905
+ tools: status?.tools ?? [],
906
+ });
907
+ }
908
+ if (result.status === 'pending') {
909
+ return c.json({ ok: true, status: 'pending' });
910
+ }
911
+ copilotMCPSessions.delete(sessionId);
912
+ return c.json({
913
+ ok: false,
914
+ status: 'error',
915
+ error: result.status === 'error' ? result.error : 'Unknown error',
916
+ });
917
+ } catch (err) {
918
+ const msg = err instanceof Error ? err.message : String(err);
919
+ return c.json({ ok: false, error: msg }, 500);
920
+ }
291
921
  }
292
922
 
293
- const authUrl = await manager.initiateAuth(serverConfig);
294
- if (authUrl) {
295
- return c.json({ ok: true, authUrl, name });
923
+ if (!code) {
924
+ return c.json({ ok: false, error: 'code is required' }, 400);
296
925
  }
297
- return c.json({
298
- ok: true,
299
- name,
300
- message: 'Already authenticated or no auth required',
301
- });
302
- } catch (err) {
303
- const msg = err instanceof Error ? err.message : String(err);
304
- return c.json({ ok: false, error: msg }, 500);
305
- }
306
- });
307
-
308
- app.post('/v1/mcp/servers/:name/auth/callback', async (c) => {
309
- const name = c.req.param('name');
310
- const body = await c.req.json();
311
- const { code, sessionId } = body;
312
-
313
- if (sessionId) {
314
- const session = copilotMCPSessions.get(sessionId);
315
- if (!session || session.serverName !== name) {
316
- return c.json({ ok: false, error: 'Session expired or invalid' }, 400);
926
+
927
+ const manager = getMCPManager();
928
+ if (!manager) {
929
+ return c.json({ ok: false, error: 'No MCP manager active' }, 400);
317
930
  }
931
+
318
932
  try {
319
- const result = await pollForCopilotTokenOnce(session.deviceCode);
320
- if (result.status === 'complete') {
321
- copilotMCPSessions.delete(sessionId);
322
- const projectRoot = process.cwd();
323
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
324
- const serverConfig = config.servers.find((s) => s.name === name);
325
- if (!serverConfig) {
326
- return c.json(
327
- { ok: false, error: `Server "${name}" not found` },
328
- 404,
329
- );
330
- }
331
- await copilotMCPOAuthStore.saveTokens(
332
- getCopilotMCPOAuthKey(
333
- name,
334
- serverConfig.scope ?? 'global',
335
- projectRoot,
336
- ),
337
- {
338
- access_token: result.accessToken,
339
- scope: COPILOT_MCP_SCOPE,
340
- },
933
+ const success = await manager.completeAuth(name, String(code));
934
+ if (success) {
935
+ const status = (await manager.getStatusAsync()).find(
936
+ (s) => s.name === name,
341
937
  );
342
- let mcpMgr = getMCPManager();
343
- if (!mcpMgr) {
344
- mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
345
- }
346
- await mcpMgr.restartServer(serverConfig);
347
- mcpMgr = getMCPManager();
348
- const status = mcpMgr
349
- ? (await mcpMgr.getStatusAsync()).find((s) => s.name === name)
350
- : undefined;
351
938
  return c.json({
352
939
  ok: true,
353
- status: 'complete',
354
940
  name,
355
941
  connected: status?.connected ?? false,
356
942
  tools: status?.tools ?? [],
357
943
  });
358
944
  }
359
- if (result.status === 'pending') {
360
- return c.json({ ok: true, status: 'pending' });
361
- }
362
- copilotMCPSessions.delete(sessionId);
363
- return c.json({
364
- ok: false,
365
- status: 'error',
366
- error: result.status === 'error' ? result.error : 'Unknown error',
367
- });
945
+ return c.json({ ok: false, error: 'Auth completion failed' }, 500);
368
946
  } catch (err) {
369
947
  const msg = err instanceof Error ? err.message : String(err);
370
948
  return c.json({ ok: false, error: msg }, 500);
371
949
  }
372
- }
950
+ },
951
+ );
373
952
 
374
- if (!code) {
375
- return c.json({ ok: false, error: 'code is required' }, 400);
376
- }
953
+ openApiRoute(
954
+ app,
955
+ {
956
+ method: 'get',
957
+ path: '/v1/mcp/servers/{name}/auth/status',
958
+ tags: ['mcp'],
959
+ operationId: 'getMCPAuthStatus',
960
+ summary: 'Get auth status for an MCP server',
961
+ parameters: [
962
+ {
963
+ in: 'path',
964
+ name: 'name',
965
+ required: true,
966
+ schema: {
967
+ type: 'string',
968
+ },
969
+ description: 'MCP server name',
970
+ },
971
+ ],
972
+ responses: {
973
+ '200': {
974
+ description: 'OK',
975
+ content: {
976
+ 'application/json': {
977
+ schema: {
978
+ type: 'object',
979
+ properties: {
980
+ authenticated: {
981
+ type: 'boolean',
982
+ },
983
+ authType: {
984
+ type: 'string',
985
+ },
986
+ },
987
+ required: ['authenticated'],
988
+ },
989
+ },
990
+ },
991
+ },
992
+ },
993
+ },
994
+ async (c) => {
995
+ const name = c.req.param('name');
996
+ const projectRoot = process.cwd();
997
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
998
+ const serverConfig = config.servers.find((s) => s.name === name);
377
999
 
378
- const manager = getMCPManager();
379
- if (!manager) {
380
- return c.json({ ok: false, error: 'No MCP manager active' }, 400);
381
- }
1000
+ if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
1001
+ try {
1002
+ const auth = await getStoredCopilotMCPToken(
1003
+ copilotMCPOAuthStore,
1004
+ name,
1005
+ serverConfig.scope ?? 'global',
1006
+ projectRoot,
1007
+ );
1008
+ const authenticated = !!auth.token && !auth.needsReauth;
1009
+ return c.json({ authenticated, authType: 'copilot-device' });
1010
+ } catch {
1011
+ return c.json({ authenticated: false, authType: 'copilot-device' });
1012
+ }
1013
+ }
382
1014
 
383
- try {
384
- const success = await manager.completeAuth(name, String(code));
385
- if (success) {
386
- const status = (await manager.getStatusAsync()).find(
387
- (s) => s.name === name,
388
- );
389
- return c.json({
390
- ok: true,
391
- name,
392
- connected: status?.connected ?? false,
393
- tools: status?.tools ?? [],
394
- });
1015
+ const manager = getMCPManager();
1016
+ if (!manager) {
1017
+ return c.json({ authenticated: false });
395
1018
  }
396
- return c.json({ ok: false, error: 'Auth completion failed' }, 500);
397
- } catch (err) {
398
- const msg = err instanceof Error ? err.message : String(err);
399
- return c.json({ ok: false, error: msg }, 500);
400
- }
401
- });
402
-
403
- app.get('/v1/mcp/servers/:name/auth/status', async (c) => {
404
- const name = c.req.param('name');
405
- const projectRoot = process.cwd();
406
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
407
- const serverConfig = config.servers.find((s) => s.name === name);
408
-
409
- if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
1019
+
410
1020
  try {
411
- const auth = await getStoredCopilotMCPToken(
412
- copilotMCPOAuthStore,
413
- name,
414
- serverConfig.scope ?? 'global',
415
- projectRoot,
416
- );
417
- const authenticated = !!auth.token && !auth.needsReauth;
418
- return c.json({ authenticated, authType: 'copilot-device' });
1021
+ const status = await manager.getAuthStatus(name);
1022
+ return c.json(status);
419
1023
  } catch {
420
- return c.json({ authenticated: false, authType: 'copilot-device' });
1024
+ return c.json({ authenticated: false });
421
1025
  }
422
- }
423
-
424
- const manager = getMCPManager();
425
- if (!manager) {
426
- return c.json({ authenticated: false });
427
- }
428
-
429
- try {
430
- const status = await manager.getAuthStatus(name);
431
- return c.json(status);
432
- } catch {
433
- return c.json({ authenticated: false });
434
- }
435
- });
436
-
437
- app.delete('/v1/mcp/servers/:name/auth', async (c) => {
438
- const name = c.req.param('name');
439
- const projectRoot = process.cwd();
440
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
441
- const serverConfig = config.servers.find((s) => s.name === name);
442
-
443
- if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
444
- try {
445
- const key = getCopilotMCPOAuthKey(
446
- name,
447
- serverConfig.scope ?? 'global',
448
- projectRoot,
449
- );
450
- await copilotMCPOAuthStore.clearServer(key);
451
- if (key !== name) {
452
- await copilotMCPOAuthStore.clearServer(name);
453
- }
454
- const manager = getMCPManager();
455
- if (manager) {
456
- await manager.clearAuthData(
1026
+ },
1027
+ );
1028
+
1029
+ openApiRoute(
1030
+ app,
1031
+ {
1032
+ method: 'delete',
1033
+ path: '/v1/mcp/servers/{name}/auth',
1034
+ tags: ['mcp'],
1035
+ operationId: 'revokeMCPAuth',
1036
+ summary: 'Revoke auth for an MCP server',
1037
+ parameters: [
1038
+ {
1039
+ in: 'path',
1040
+ name: 'name',
1041
+ required: true,
1042
+ schema: {
1043
+ type: 'string',
1044
+ },
1045
+ description: 'MCP server name',
1046
+ },
1047
+ ],
1048
+ responses: {
1049
+ '200': {
1050
+ description: 'OK',
1051
+ content: {
1052
+ 'application/json': {
1053
+ schema: {
1054
+ type: 'object',
1055
+ properties: {
1056
+ ok: {
1057
+ type: 'boolean',
1058
+ },
1059
+ error: {
1060
+ type: 'string',
1061
+ },
1062
+ },
1063
+ required: ['ok'],
1064
+ },
1065
+ },
1066
+ },
1067
+ },
1068
+ '400': {
1069
+ description: 'Bad Request',
1070
+ content: {
1071
+ 'application/json': {
1072
+ schema: {
1073
+ type: 'object',
1074
+ properties: {
1075
+ error: {
1076
+ type: 'string',
1077
+ },
1078
+ },
1079
+ required: ['error'],
1080
+ },
1081
+ },
1082
+ },
1083
+ },
1084
+ },
1085
+ },
1086
+ async (c) => {
1087
+ const name = c.req.param('name');
1088
+ const projectRoot = process.cwd();
1089
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
1090
+ const serverConfig = config.servers.find((s) => s.name === name);
1091
+
1092
+ if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
1093
+ try {
1094
+ const key = getCopilotMCPOAuthKey(
457
1095
  name,
458
1096
  serverConfig.scope ?? 'global',
459
1097
  projectRoot,
460
1098
  );
461
- await manager.stopServer(name);
1099
+ await copilotMCPOAuthStore.clearServer(key);
1100
+ if (key !== name) {
1101
+ await copilotMCPOAuthStore.clearServer(name);
1102
+ }
1103
+ const manager = getMCPManager();
1104
+ if (manager) {
1105
+ await manager.clearAuthData(
1106
+ name,
1107
+ serverConfig.scope ?? 'global',
1108
+ projectRoot,
1109
+ );
1110
+ await manager.stopServer(name);
1111
+ }
1112
+ return c.json({ ok: true, name });
1113
+ } catch (err) {
1114
+ const msg = err instanceof Error ? err.message : String(err);
1115
+ return c.json({ ok: false, error: msg }, 500);
462
1116
  }
1117
+ }
1118
+
1119
+ const manager = getMCPManager();
1120
+ if (!manager) {
1121
+ return c.json({ ok: false, error: 'No MCP manager active' }, 400);
1122
+ }
1123
+
1124
+ try {
1125
+ await manager.revokeAuth(name);
463
1126
  return c.json({ ok: true, name });
464
1127
  } catch (err) {
465
1128
  const msg = err instanceof Error ? err.message : String(err);
466
1129
  return c.json({ ok: false, error: msg }, 500);
467
1130
  }
468
- }
469
-
470
- const manager = getMCPManager();
471
- if (!manager) {
472
- return c.json({ ok: false, error: 'No MCP manager active' }, 400);
473
- }
474
-
475
- try {
476
- await manager.revokeAuth(name);
477
- return c.json({ ok: true, name });
478
- } catch (err) {
479
- const msg = err instanceof Error ? err.message : String(err);
480
- return c.json({ ok: false, error: msg }, 500);
481
- }
482
- });
483
-
484
- app.post('/v1/mcp/servers/:name/test', async (c) => {
485
- const name = c.req.param('name');
486
- const projectRoot = process.cwd();
487
- const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
488
- const serverConfig = config.servers.find((s) => s.name === name);
489
-
490
- if (!serverConfig) {
491
- return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
492
- }
493
-
494
- const client = new MCPClientWrapper(serverConfig);
495
- try {
496
- await client.connect();
497
- const tools = await client.listTools();
498
- await client.disconnect();
499
- return c.json({
500
- ok: true,
501
- name,
502
- tools: tools.map((t) => ({
503
- name: t.name,
504
- description: t.description,
505
- })),
506
- });
507
- } catch (err) {
508
- const msg = err instanceof Error ? err.message : String(err);
509
- return c.json({ ok: false, error: msg }, 500);
510
- }
511
- });
1131
+ },
1132
+ );
1133
+
1134
+ openApiRoute(
1135
+ app,
1136
+ {
1137
+ method: 'post',
1138
+ path: '/v1/mcp/servers/{name}/test',
1139
+ tags: ['mcp'],
1140
+ operationId: 'testMCPServer',
1141
+ summary: 'Test connection to an MCP server',
1142
+ parameters: [
1143
+ {
1144
+ in: 'path',
1145
+ name: 'name',
1146
+ required: true,
1147
+ schema: {
1148
+ type: 'string',
1149
+ },
1150
+ description: 'MCP server name',
1151
+ },
1152
+ ],
1153
+ responses: {
1154
+ '200': {
1155
+ description: 'OK',
1156
+ content: {
1157
+ 'application/json': {
1158
+ schema: {
1159
+ type: 'object',
1160
+ properties: {
1161
+ ok: {
1162
+ type: 'boolean',
1163
+ },
1164
+ name: {
1165
+ type: 'string',
1166
+ },
1167
+ tools: {
1168
+ type: 'array',
1169
+ items: {
1170
+ type: 'object',
1171
+ properties: {
1172
+ name: {
1173
+ type: 'string',
1174
+ },
1175
+ description: {
1176
+ type: 'string',
1177
+ },
1178
+ },
1179
+ },
1180
+ },
1181
+ error: {
1182
+ type: 'string',
1183
+ },
1184
+ },
1185
+ required: ['ok'],
1186
+ },
1187
+ },
1188
+ },
1189
+ },
1190
+ '404': {
1191
+ description: 'Bad Request',
1192
+ content: {
1193
+ 'application/json': {
1194
+ schema: {
1195
+ type: 'object',
1196
+ properties: {
1197
+ error: {
1198
+ type: 'string',
1199
+ },
1200
+ },
1201
+ required: ['error'],
1202
+ },
1203
+ },
1204
+ },
1205
+ },
1206
+ },
1207
+ },
1208
+ async (c) => {
1209
+ const name = c.req.param('name');
1210
+ const projectRoot = process.cwd();
1211
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
1212
+ const serverConfig = config.servers.find((s) => s.name === name);
1213
+
1214
+ if (!serverConfig) {
1215
+ return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
1216
+ }
1217
+
1218
+ const client = new MCPClientWrapper(serverConfig);
1219
+ try {
1220
+ await client.connect();
1221
+ const tools = await client.listTools();
1222
+ await client.disconnect();
1223
+ return c.json({
1224
+ ok: true,
1225
+ name,
1226
+ tools: tools.map((t) => ({
1227
+ name: t.name,
1228
+ description: t.description,
1229
+ })),
1230
+ });
1231
+ } catch (err) {
1232
+ const msg = err instanceof Error ? err.message : String(err);
1233
+ return c.json({ ok: false, error: msg }, 500);
1234
+ }
1235
+ },
1236
+ );
512
1237
  }