@ottocode/server 0.1.264 → 0.1.266

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 (74) hide show
  1. package/package.json +3 -3
  2. package/src/routes/auth/copilot.ts +699 -0
  3. package/src/routes/auth/oauth.ts +578 -0
  4. package/src/routes/auth/onboarding.ts +45 -0
  5. package/src/routes/auth/providers.ts +189 -0
  6. package/src/routes/auth/service.ts +167 -0
  7. package/src/routes/auth/state.ts +23 -0
  8. package/src/routes/auth/status.ts +203 -0
  9. package/src/routes/auth/wallet.ts +229 -0
  10. package/src/routes/auth.ts +12 -2080
  11. package/src/routes/config/models-service.ts +411 -0
  12. package/src/routes/config/models.ts +6 -426
  13. package/src/routes/config/providers-service.ts +237 -0
  14. package/src/routes/config/providers.ts +10 -242
  15. package/src/routes/files/handlers.ts +297 -0
  16. package/src/routes/files/service.ts +313 -0
  17. package/src/routes/files.ts +12 -608
  18. package/src/routes/git/commit-service.ts +207 -0
  19. package/src/routes/git/commit.ts +6 -220
  20. package/src/routes/git/remote-service.ts +116 -0
  21. package/src/routes/git/remote.ts +8 -115
  22. package/src/routes/git/staging-service.ts +111 -0
  23. package/src/routes/git/staging.ts +10 -205
  24. package/src/routes/mcp/auth.ts +338 -0
  25. package/src/routes/mcp/lifecycle.ts +263 -0
  26. package/src/routes/mcp/servers.ts +212 -0
  27. package/src/routes/mcp/service.ts +664 -0
  28. package/src/routes/mcp/state.ts +13 -0
  29. package/src/routes/mcp.ts +6 -1233
  30. package/src/routes/ottorouter/billing.ts +593 -0
  31. package/src/routes/ottorouter/service.ts +92 -0
  32. package/src/routes/ottorouter/topup.ts +301 -0
  33. package/src/routes/ottorouter/wallet.ts +370 -0
  34. package/src/routes/ottorouter.ts +6 -1319
  35. package/src/routes/research/service.ts +339 -0
  36. package/src/routes/research.ts +12 -390
  37. package/src/routes/sessions/crud.ts +563 -0
  38. package/src/routes/sessions/queue.ts +242 -0
  39. package/src/routes/sessions/retry.ts +121 -0
  40. package/src/routes/sessions/service.ts +768 -0
  41. package/src/routes/sessions/share.ts +434 -0
  42. package/src/routes/sessions.ts +8 -1977
  43. package/src/routes/skills/service.ts +221 -0
  44. package/src/routes/skills/spec.ts +309 -0
  45. package/src/routes/skills.ts +31 -909
  46. package/src/routes/terminals/service.ts +326 -0
  47. package/src/routes/terminals.ts +19 -295
  48. package/src/routes/tunnel/service.ts +217 -0
  49. package/src/routes/tunnel.ts +29 -219
  50. package/src/runtime/agent/registry-prompts.ts +147 -0
  51. package/src/runtime/agent/registry.ts +6 -124
  52. package/src/runtime/agent/runner-errors.ts +116 -0
  53. package/src/runtime/agent/runner-reminders.ts +45 -0
  54. package/src/runtime/agent/runner-setup-model.ts +75 -0
  55. package/src/runtime/agent/runner-setup-prompt.ts +185 -0
  56. package/src/runtime/agent/runner-setup-tools.ts +103 -0
  57. package/src/runtime/agent/runner-setup-utils.ts +21 -0
  58. package/src/runtime/agent/runner-setup.ts +54 -288
  59. package/src/runtime/agent/runner-telemetry.ts +112 -0
  60. package/src/runtime/agent/runner-text.ts +108 -0
  61. package/src/runtime/agent/runner-tool-observer.ts +86 -0
  62. package/src/runtime/agent/runner.ts +79 -378
  63. package/src/runtime/ask/service.ts +1 -0
  64. package/src/runtime/provider/custom.ts +73 -0
  65. package/src/runtime/provider/index.ts +6 -85
  66. package/src/runtime/provider/reasoning-builders.ts +280 -0
  67. package/src/runtime/provider/reasoning.ts +68 -264
  68. package/src/runtime/provider/xai.ts +8 -0
  69. package/src/tools/adapter/events.ts +116 -0
  70. package/src/tools/adapter/execution.ts +160 -0
  71. package/src/tools/adapter/pending.ts +37 -0
  72. package/src/tools/adapter/persistence.ts +166 -0
  73. package/src/tools/adapter/results.ts +97 -0
  74. package/src/tools/adapter.ts +124 -451
package/src/routes/mcp.ts CHANGED
@@ -1,1237 +1,10 @@
1
1
  import type { Hono } from 'hono';
2
- import {
3
- COPILOT_MCP_SCOPE,
4
- getMCPManager,
5
- getCopilotMCPOAuthKey,
6
- getStoredCopilotMCPToken,
7
- initializeMCP,
8
- isGitHubCopilotUrl,
9
- loadMCPConfig,
10
- getGlobalConfigDir,
11
- MCPClientWrapper,
12
- OAuthCredentialStore,
13
- addMCPServerToConfig,
14
- removeMCPServerFromConfig,
15
- } from '@ottocode/sdk';
16
- import { authorizeCopilot, pollForCopilotTokenOnce } from '@ottocode/sdk';
17
- import { openApiRoute } from '../openapi/route.ts';
18
-
19
- const copilotMCPOAuthStore = new OAuthCredentialStore();
20
-
21
- const copilotMCPSessions = new Map<
22
- string,
23
- {
24
- deviceCode: string;
25
- interval: number;
26
- serverName: string;
27
- createdAt: number;
28
- }
29
- >();
2
+ import { registerMCPAuthRoutes } from './mcp/auth.ts';
3
+ import { registerMCPLifecycleRoutes } from './mcp/lifecycle.ts';
4
+ import { registerMCPServerConfigRoutes } from './mcp/servers.ts';
30
5
 
31
6
  export function registerMCPRoutes(app: Hono) {
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 } : {}),
244
- };
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: [
269
- {
270
- in: 'path',
271
- name: 'name',
272
- required: true,
273
- schema: {
274
- type: 'string',
275
- },
276
- description: 'MCP server name',
277
- },
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);
346
- }
347
- },
348
- );
349
-
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) {
455
- return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
456
- }
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
-
578
- if (!manager) {
579
- return c.json({ ok: false, error: 'No MCP manager active' }, 400);
580
- }
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);
684
- }
685
-
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
-
704
- const deviceData = await authorizeCopilot({ mcp: true });
705
- const sessionId = crypto.randomUUID();
706
- copilotMCPSessions.set(sessionId, {
707
- deviceCode: deviceData.deviceCode,
708
- interval: deviceData.interval,
709
- serverName: name,
710
- createdAt: Date.now(),
711
- });
712
- return c.json({
713
- ok: true,
714
- name,
715
- authType: 'copilot-device',
716
- sessionId,
717
- userCode: deviceData.userCode,
718
- verificationUri: deviceData.verificationUri,
719
- interval: deviceData.interval,
720
- });
721
- } catch (err) {
722
- const msg = err instanceof Error ? err.message : String(err);
723
- return c.json({ ok: false, error: msg }, 500);
724
- }
725
- }
726
-
727
- try {
728
- let manager = getMCPManager();
729
- if (!manager) {
730
- manager = await initializeMCP({ servers: [] }, projectRoot);
731
- }
732
- if (!manager.started) {
733
- manager.setProjectRoot(projectRoot);
734
- }
735
-
736
- const authUrl = await manager.initiateAuth(serverConfig);
737
- if (authUrl) {
738
- return c.json({ ok: true, authUrl, name });
739
- }
740
- return c.json({
741
- ok: true,
742
- name,
743
- message: 'Already authenticated or no auth required',
744
- });
745
- } catch (err) {
746
- const msg = err instanceof Error ? err.message : String(err);
747
- return c.json({ ok: false, error: msg }, 500);
748
- }
749
- },
750
- );
751
-
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
- }
921
- }
922
-
923
- if (!code) {
924
- return c.json({ ok: false, error: 'code is required' }, 400);
925
- }
926
-
927
- const manager = getMCPManager();
928
- if (!manager) {
929
- return c.json({ ok: false, error: 'No MCP manager active' }, 400);
930
- }
931
-
932
- try {
933
- const success = await manager.completeAuth(name, String(code));
934
- if (success) {
935
- const status = (await manager.getStatusAsync()).find(
936
- (s) => s.name === name,
937
- );
938
- return c.json({
939
- ok: true,
940
- name,
941
- connected: status?.connected ?? false,
942
- tools: status?.tools ?? [],
943
- });
944
- }
945
- return c.json({ ok: false, error: 'Auth completion failed' }, 500);
946
- } catch (err) {
947
- const msg = err instanceof Error ? err.message : String(err);
948
- return c.json({ ok: false, error: msg }, 500);
949
- }
950
- },
951
- );
952
-
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);
999
-
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
- }
1014
-
1015
- const manager = getMCPManager();
1016
- if (!manager) {
1017
- return c.json({ authenticated: false });
1018
- }
1019
-
1020
- try {
1021
- const status = await manager.getAuthStatus(name);
1022
- return c.json(status);
1023
- } catch {
1024
- return c.json({ authenticated: false });
1025
- }
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(
1095
- name,
1096
- serverConfig.scope ?? 'global',
1097
- projectRoot,
1098
- );
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);
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);
1126
- return c.json({ ok: true, name });
1127
- } catch (err) {
1128
- const msg = err instanceof Error ? err.message : String(err);
1129
- return c.json({ ok: false, error: msg }, 500);
1130
- }
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
- );
7
+ registerMCPServerConfigRoutes(app);
8
+ registerMCPLifecycleRoutes(app);
9
+ registerMCPAuthRoutes(app);
1237
10
  }