@ottocode/server 0.1.260 → 0.1.262
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 +4 -3
- package/src/index.ts +5 -4
- package/src/openapi/register.ts +92 -0
- package/src/openapi/route.ts +22 -0
- package/src/routes/ask.ts +210 -99
- package/src/routes/auth.ts +1701 -626
- package/src/routes/branch.ts +281 -90
- package/src/routes/config/agents.ts +79 -32
- package/src/routes/config/cwd.ts +46 -14
- package/src/routes/config/debug.ts +159 -30
- package/src/routes/config/defaults.ts +182 -64
- package/src/routes/config/main.ts +109 -73
- package/src/routes/config/models.ts +304 -137
- package/src/routes/config/providers.ts +462 -166
- package/src/routes/config/utils.ts +2 -2
- package/src/routes/doctor.ts +395 -161
- package/src/routes/files.ts +650 -260
- package/src/routes/git/branch.ts +143 -52
- package/src/routes/git/commit.ts +347 -141
- package/src/routes/git/diff.ts +239 -116
- package/src/routes/git/init.ts +103 -23
- package/src/routes/git/pull.ts +167 -65
- package/src/routes/git/push.ts +222 -117
- package/src/routes/git/remote.ts +401 -100
- package/src/routes/git/staging.ts +502 -141
- package/src/routes/git/status.ts +171 -78
- package/src/routes/mcp.ts +1129 -404
- package/src/routes/openapi.ts +27 -4
- package/src/routes/ottorouter.ts +1221 -389
- package/src/routes/provider-usage.ts +153 -36
- package/src/routes/research.ts +817 -370
- package/src/routes/root.ts +50 -6
- package/src/routes/session-approval.ts +228 -54
- package/src/routes/session-files.ts +265 -134
- package/src/routes/session-messages.ts +330 -150
- package/src/routes/session-stream.ts +83 -2
- package/src/routes/sessions.ts +1830 -780
- package/src/routes/skills.ts +849 -161
- package/src/routes/terminals.ts +469 -103
- package/src/routes/tunnel.ts +394 -118
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/message/compaction-limits.ts +3 -3
- package/src/runtime/provider/reasoning.ts +2 -1
- package/src/runtime/session/db-operations.ts +4 -3
- package/src/runtime/utils/token.ts +7 -2
- package/src/tools/adapter.ts +21 -0
- package/src/openapi/paths/ask.ts +0 -81
- package/src/openapi/paths/auth.ts +0 -687
- package/src/openapi/paths/branch.ts +0 -102
- package/src/openapi/paths/config.ts +0 -485
- package/src/openapi/paths/doctor.ts +0 -165
- package/src/openapi/paths/files.ts +0 -236
- package/src/openapi/paths/git.ts +0 -690
- package/src/openapi/paths/mcp.ts +0 -339
- package/src/openapi/paths/messages.ts +0 -103
- package/src/openapi/paths/ottorouter.ts +0 -594
- package/src/openapi/paths/provider-usage.ts +0 -59
- package/src/openapi/paths/research.ts +0 -227
- package/src/openapi/paths/session-approval.ts +0 -93
- package/src/openapi/paths/session-extras.ts +0 -336
- package/src/openapi/paths/session-files.ts +0 -91
- package/src/openapi/paths/sessions.ts +0 -210
- package/src/openapi/paths/skills.ts +0 -377
- package/src/openapi/paths/stream.ts +0 -26
- package/src/openapi/paths/terminals.ts +0 -226
- package/src/openapi/paths/tunnel.ts +0 -163
- 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
270
|
+
in: 'path',
|
|
271
|
+
name: 'name',
|
|
272
|
+
required: true,
|
|
273
|
+
schema: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
},
|
|
276
|
+
description: 'MCP server name',
|
|
81
277
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
579
|
+
return c.json({ ok: false, error: 'No MCP manager active' }, 400);
|
|
162
580
|
}
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
|
412
|
-
|
|
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
|
|
1024
|
+
return c.json({ authenticated: false });
|
|
421
1025
|
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
}
|