@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.
- package/package.json +3 -3
- package/src/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +6 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +68 -264
- package/src/runtime/provider/xai.ts +8 -0
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COPILOT_MCP_SCOPE,
|
|
3
|
+
addMCPServerToConfig,
|
|
4
|
+
authorizeCopilot,
|
|
5
|
+
getCopilotMCPOAuthKey,
|
|
6
|
+
getGlobalConfigDir,
|
|
7
|
+
getMCPManager,
|
|
8
|
+
getStoredCopilotMCPToken,
|
|
9
|
+
initializeMCP,
|
|
10
|
+
isGitHubCopilotUrl,
|
|
11
|
+
loadMCPConfig,
|
|
12
|
+
MCPClientWrapper,
|
|
13
|
+
pollForCopilotTokenOnce,
|
|
14
|
+
removeMCPServerFromConfig,
|
|
15
|
+
type MCPServerConfig,
|
|
16
|
+
type MCPTransport,
|
|
17
|
+
type OAuthCredentialStore,
|
|
18
|
+
} from '@ottocode/sdk';
|
|
19
|
+
|
|
20
|
+
type CopilotMCPSession = {
|
|
21
|
+
deviceCode: string;
|
|
22
|
+
interval: number;
|
|
23
|
+
serverName: string;
|
|
24
|
+
createdAt: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function listMCPServers(projectRoot = process.cwd()) {
|
|
28
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
29
|
+
const manager = getMCPManager();
|
|
30
|
+
const statuses = manager ? await manager.getStatusAsync() : [];
|
|
31
|
+
|
|
32
|
+
return config.servers.map((server) => {
|
|
33
|
+
const status = statuses.find((item) => item.name === server.name);
|
|
34
|
+
return {
|
|
35
|
+
name: server.name,
|
|
36
|
+
transport: server.transport ?? 'stdio',
|
|
37
|
+
command: server.command,
|
|
38
|
+
args: server.args ?? [],
|
|
39
|
+
url: server.url,
|
|
40
|
+
disabled: server.disabled ?? false,
|
|
41
|
+
connected: status?.connected ?? false,
|
|
42
|
+
tools: status?.tools ?? [],
|
|
43
|
+
authRequired: status?.authRequired ?? false,
|
|
44
|
+
authenticated: status?.authenticated ?? false,
|
|
45
|
+
scope: server.scope ?? 'global',
|
|
46
|
+
...(isGitHubCopilotUrl(server.url) ? { authType: 'copilot-device' } : {}),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildMCPServerConfig(body: Record<string, unknown>) {
|
|
52
|
+
const { name, transport, command, args, env, url, headers, oauth, scope } =
|
|
53
|
+
body;
|
|
54
|
+
if (!name) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false as const,
|
|
57
|
+
error: 'name is required',
|
|
58
|
+
status: 400 as const,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const t: MCPTransport =
|
|
63
|
+
transport === 'http' || transport === 'sse' ? transport : 'stdio';
|
|
64
|
+
if (t === 'stdio' && !command) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false as const,
|
|
67
|
+
error: 'command is required for stdio transport',
|
|
68
|
+
status: 400 as const,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (t === 'stdio' && command && /^https?:\/\//i.test(String(command))) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false as const,
|
|
74
|
+
error:
|
|
75
|
+
'stdio transport requires a local command, not a URL. Use http or sse transport for remote servers.',
|
|
76
|
+
status: 400 as const,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if ((t === 'http' || t === 'sse') && !url) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false as const,
|
|
82
|
+
error: 'url is required for http/sse transport',
|
|
83
|
+
status: 400 as const,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const serverScope = scope === 'project' ? 'project' : 'global';
|
|
88
|
+
const serverConfig: MCPServerConfig = {
|
|
89
|
+
name: String(name),
|
|
90
|
+
transport: t,
|
|
91
|
+
scope: serverScope,
|
|
92
|
+
...(command ? { command: String(command) } : {}),
|
|
93
|
+
...(Array.isArray(args) ? { args: args.map(String) } : {}),
|
|
94
|
+
...(env && typeof env === 'object'
|
|
95
|
+
? { env: env as Record<string, string> }
|
|
96
|
+
: {}),
|
|
97
|
+
...(url ? { url: String(url) } : {}),
|
|
98
|
+
...(headers && typeof headers === 'object'
|
|
99
|
+
? { headers: headers as Record<string, string> }
|
|
100
|
+
: {}),
|
|
101
|
+
...(oauth && typeof oauth === 'object'
|
|
102
|
+
? { oauth: oauth as MCPServerConfig['oauth'] }
|
|
103
|
+
: {}),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
ok: true as const,
|
|
108
|
+
serverConfig,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function addMCPServer(
|
|
113
|
+
body: Record<string, unknown>,
|
|
114
|
+
projectRoot = process.cwd(),
|
|
115
|
+
) {
|
|
116
|
+
const built = buildMCPServerConfig(body);
|
|
117
|
+
if (!built.ok)
|
|
118
|
+
return { ok: false as const, body: built, status: built.status };
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await addMCPServerToConfig(
|
|
122
|
+
projectRoot,
|
|
123
|
+
built.serverConfig,
|
|
124
|
+
getGlobalConfigDir(),
|
|
125
|
+
);
|
|
126
|
+
return {
|
|
127
|
+
ok: true as const,
|
|
128
|
+
body: { ok: true, server: built.serverConfig },
|
|
129
|
+
};
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
132
|
+
return {
|
|
133
|
+
ok: false as const,
|
|
134
|
+
body: { ok: false, error: msg },
|
|
135
|
+
status: 500 as const,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function removeMCPServer(
|
|
141
|
+
name: string,
|
|
142
|
+
projectRoot = process.cwd(),
|
|
143
|
+
) {
|
|
144
|
+
try {
|
|
145
|
+
const manager = getMCPManager();
|
|
146
|
+
if (manager) {
|
|
147
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
148
|
+
const serverConfig = config.servers.find(
|
|
149
|
+
(server) => server.name === name,
|
|
150
|
+
);
|
|
151
|
+
const scope = serverConfig?.scope ?? 'global';
|
|
152
|
+
await manager.clearAuthData(name, scope, projectRoot);
|
|
153
|
+
await manager.stopServer(name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const removed = await removeMCPServerFromConfig(
|
|
157
|
+
projectRoot,
|
|
158
|
+
name,
|
|
159
|
+
getGlobalConfigDir(),
|
|
160
|
+
);
|
|
161
|
+
if (!removed) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false as const,
|
|
164
|
+
body: { ok: false, error: `Server "${name}" not found` },
|
|
165
|
+
status: 404 as const,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { ok: true as const, body: { ok: true, name } };
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
+
return {
|
|
172
|
+
ok: false as const,
|
|
173
|
+
body: { ok: false, error: msg },
|
|
174
|
+
status: 500 as const,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function startMCPServer(options: {
|
|
180
|
+
name: string;
|
|
181
|
+
projectRoot?: string;
|
|
182
|
+
oAuthStore: OAuthCredentialStore;
|
|
183
|
+
sessions: Map<string, CopilotMCPSession>;
|
|
184
|
+
}) {
|
|
185
|
+
const { name, oAuthStore, sessions } = options;
|
|
186
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
187
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
188
|
+
const serverConfig = config.servers.find((server) => server.name === name);
|
|
189
|
+
|
|
190
|
+
if (!serverConfig) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false as const,
|
|
193
|
+
body: { ok: false, error: `Server "${name}" not found` },
|
|
194
|
+
status: 404 as const,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
let manager = getMCPManager();
|
|
200
|
+
if (!manager) {
|
|
201
|
+
manager = await initializeMCP({ servers: [] }, projectRoot);
|
|
202
|
+
}
|
|
203
|
+
if (!manager.started) {
|
|
204
|
+
manager.setProjectRoot(projectRoot);
|
|
205
|
+
}
|
|
206
|
+
await manager.restartServer(serverConfig);
|
|
207
|
+
const status = (await manager.getStatusAsync()).find(
|
|
208
|
+
(server) => server.name === name,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (isGitHubCopilotUrl(serverConfig.url) && !status?.connected) {
|
|
212
|
+
const existingAuth = await getStoredCopilotMCPToken(
|
|
213
|
+
oAuthStore,
|
|
214
|
+
name,
|
|
215
|
+
serverConfig.scope ?? 'global',
|
|
216
|
+
projectRoot,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!existingAuth.token || existingAuth.needsReauth) {
|
|
220
|
+
const deviceData = await authorizeCopilot({ mcp: true });
|
|
221
|
+
const sessionId = crypto.randomUUID();
|
|
222
|
+
sessions.set(sessionId, {
|
|
223
|
+
deviceCode: deviceData.deviceCode,
|
|
224
|
+
interval: deviceData.interval,
|
|
225
|
+
serverName: name,
|
|
226
|
+
createdAt: Date.now(),
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
ok: true as const,
|
|
230
|
+
body: {
|
|
231
|
+
ok: true,
|
|
232
|
+
name,
|
|
233
|
+
connected: false,
|
|
234
|
+
authRequired: true,
|
|
235
|
+
authType: 'copilot-device',
|
|
236
|
+
sessionId,
|
|
237
|
+
userCode: deviceData.userCode,
|
|
238
|
+
verificationUri: deviceData.verificationUri,
|
|
239
|
+
interval: deviceData.interval,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
ok: true as const,
|
|
247
|
+
body: {
|
|
248
|
+
ok: true,
|
|
249
|
+
name,
|
|
250
|
+
connected: status?.connected ?? false,
|
|
251
|
+
tools: status?.tools ?? [],
|
|
252
|
+
authRequired: status?.authRequired ?? false,
|
|
253
|
+
authUrl: manager.getAuthUrl(name),
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
258
|
+
return {
|
|
259
|
+
ok: false as const,
|
|
260
|
+
body: { ok: false, error: msg },
|
|
261
|
+
status: 500 as const,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function stopMCPServer(name: string) {
|
|
267
|
+
const manager = getMCPManager();
|
|
268
|
+
if (!manager) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false as const,
|
|
271
|
+
body: { ok: false, error: 'No MCP manager active' },
|
|
272
|
+
status: 400 as const,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await manager.stopServer(name);
|
|
278
|
+
return { ok: true as const, body: { ok: true, name, connected: false } };
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
281
|
+
return {
|
|
282
|
+
ok: false as const,
|
|
283
|
+
body: { ok: false, error: msg },
|
|
284
|
+
status: 500 as const,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function initiateMCPAuth(options: {
|
|
290
|
+
name: string;
|
|
291
|
+
projectRoot?: string;
|
|
292
|
+
oAuthStore: OAuthCredentialStore;
|
|
293
|
+
sessions: Map<string, CopilotMCPSession>;
|
|
294
|
+
}) {
|
|
295
|
+
const { name, oAuthStore, sessions } = options;
|
|
296
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
297
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
298
|
+
const serverConfig = config.servers.find((server) => server.name === name);
|
|
299
|
+
|
|
300
|
+
if (!serverConfig) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false as const,
|
|
303
|
+
body: { ok: false, error: `Server "${name}" not found` },
|
|
304
|
+
status: 404 as const,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isGitHubCopilotUrl(serverConfig.url)) {
|
|
309
|
+
try {
|
|
310
|
+
const existingAuth = await getStoredCopilotMCPToken(
|
|
311
|
+
oAuthStore,
|
|
312
|
+
name,
|
|
313
|
+
serverConfig.scope ?? 'global',
|
|
314
|
+
projectRoot,
|
|
315
|
+
);
|
|
316
|
+
if (existingAuth.token && !existingAuth.needsReauth) {
|
|
317
|
+
return {
|
|
318
|
+
ok: true as const,
|
|
319
|
+
body: {
|
|
320
|
+
ok: true,
|
|
321
|
+
name,
|
|
322
|
+
authType: 'copilot-device',
|
|
323
|
+
authenticated: true,
|
|
324
|
+
message: 'Already authenticated with MCP scopes',
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const deviceData = await authorizeCopilot({ mcp: true });
|
|
330
|
+
const sessionId = crypto.randomUUID();
|
|
331
|
+
sessions.set(sessionId, {
|
|
332
|
+
deviceCode: deviceData.deviceCode,
|
|
333
|
+
interval: deviceData.interval,
|
|
334
|
+
serverName: name,
|
|
335
|
+
createdAt: Date.now(),
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
ok: true as const,
|
|
339
|
+
body: {
|
|
340
|
+
ok: true,
|
|
341
|
+
name,
|
|
342
|
+
authType: 'copilot-device',
|
|
343
|
+
sessionId,
|
|
344
|
+
userCode: deviceData.userCode,
|
|
345
|
+
verificationUri: deviceData.verificationUri,
|
|
346
|
+
interval: deviceData.interval,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
} catch (err) {
|
|
350
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
351
|
+
return {
|
|
352
|
+
ok: false as const,
|
|
353
|
+
body: { ok: false, error: msg },
|
|
354
|
+
status: 500 as const,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
let manager = getMCPManager();
|
|
361
|
+
if (!manager) {
|
|
362
|
+
manager = await initializeMCP({ servers: [] }, projectRoot);
|
|
363
|
+
}
|
|
364
|
+
if (!manager.started) {
|
|
365
|
+
manager.setProjectRoot(projectRoot);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const authUrl = await manager.initiateAuth(serverConfig);
|
|
369
|
+
if (authUrl) {
|
|
370
|
+
return { ok: true as const, body: { ok: true, authUrl, name } };
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
ok: true as const,
|
|
374
|
+
body: {
|
|
375
|
+
ok: true,
|
|
376
|
+
name,
|
|
377
|
+
message: 'Already authenticated or no auth required',
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
} catch (err) {
|
|
381
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
382
|
+
return {
|
|
383
|
+
ok: false as const,
|
|
384
|
+
body: { ok: false, error: msg },
|
|
385
|
+
status: 500 as const,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function completeMCPAuth(options: {
|
|
391
|
+
name: string;
|
|
392
|
+
body: Record<string, unknown>;
|
|
393
|
+
projectRoot?: string;
|
|
394
|
+
oAuthStore: OAuthCredentialStore;
|
|
395
|
+
sessions: Map<string, CopilotMCPSession>;
|
|
396
|
+
}) {
|
|
397
|
+
const { name, body, oAuthStore, sessions } = options;
|
|
398
|
+
const { code, sessionId } = body;
|
|
399
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
400
|
+
|
|
401
|
+
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
402
|
+
const session = sessions.get(sessionId);
|
|
403
|
+
if (!session || session.serverName !== name) {
|
|
404
|
+
return {
|
|
405
|
+
ok: false as const,
|
|
406
|
+
body: { ok: false, error: 'Session expired or invalid' },
|
|
407
|
+
status: 400 as const,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const result = await pollForCopilotTokenOnce(session.deviceCode);
|
|
412
|
+
if (result.status === 'complete') {
|
|
413
|
+
sessions.delete(sessionId);
|
|
414
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
415
|
+
const serverConfig = config.servers.find(
|
|
416
|
+
(server) => server.name === name,
|
|
417
|
+
);
|
|
418
|
+
if (!serverConfig) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false as const,
|
|
421
|
+
body: { ok: false, error: `Server "${name}" not found` },
|
|
422
|
+
status: 404 as const,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
await oAuthStore.saveTokens(
|
|
426
|
+
getCopilotMCPOAuthKey(
|
|
427
|
+
name,
|
|
428
|
+
serverConfig.scope ?? 'global',
|
|
429
|
+
projectRoot,
|
|
430
|
+
),
|
|
431
|
+
{
|
|
432
|
+
access_token: result.accessToken,
|
|
433
|
+
scope: COPILOT_MCP_SCOPE,
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
let mcpMgr = getMCPManager();
|
|
437
|
+
if (!mcpMgr) {
|
|
438
|
+
mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
|
|
439
|
+
}
|
|
440
|
+
await mcpMgr.restartServer(serverConfig);
|
|
441
|
+
mcpMgr = getMCPManager();
|
|
442
|
+
const status = mcpMgr
|
|
443
|
+
? (await mcpMgr.getStatusAsync()).find(
|
|
444
|
+
(server) => server.name === name,
|
|
445
|
+
)
|
|
446
|
+
: undefined;
|
|
447
|
+
return {
|
|
448
|
+
ok: true as const,
|
|
449
|
+
body: {
|
|
450
|
+
ok: true,
|
|
451
|
+
status: 'complete',
|
|
452
|
+
name,
|
|
453
|
+
connected: status?.connected ?? false,
|
|
454
|
+
tools: status?.tools ?? [],
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (result.status === 'pending') {
|
|
459
|
+
return { ok: true as const, body: { ok: true, status: 'pending' } };
|
|
460
|
+
}
|
|
461
|
+
sessions.delete(sessionId);
|
|
462
|
+
return {
|
|
463
|
+
ok: true as const,
|
|
464
|
+
body: {
|
|
465
|
+
ok: false,
|
|
466
|
+
status: 'error',
|
|
467
|
+
error: result.status === 'error' ? result.error : 'Unknown error',
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
} catch (err) {
|
|
471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
+
return {
|
|
473
|
+
ok: false as const,
|
|
474
|
+
body: { ok: false, error: msg },
|
|
475
|
+
status: 500 as const,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!code) {
|
|
481
|
+
return {
|
|
482
|
+
ok: false as const,
|
|
483
|
+
body: { ok: false, error: 'code is required' },
|
|
484
|
+
status: 400 as const,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const manager = getMCPManager();
|
|
489
|
+
if (!manager) {
|
|
490
|
+
return {
|
|
491
|
+
ok: false as const,
|
|
492
|
+
body: { ok: false, error: 'No MCP manager active' },
|
|
493
|
+
status: 400 as const,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const success = await manager.completeAuth(name, String(code));
|
|
499
|
+
if (success) {
|
|
500
|
+
const status = (await manager.getStatusAsync()).find(
|
|
501
|
+
(server) => server.name === name,
|
|
502
|
+
);
|
|
503
|
+
return {
|
|
504
|
+
ok: true as const,
|
|
505
|
+
body: {
|
|
506
|
+
ok: true,
|
|
507
|
+
name,
|
|
508
|
+
connected: status?.connected ?? false,
|
|
509
|
+
tools: status?.tools ?? [],
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
ok: false as const,
|
|
515
|
+
body: { ok: false, error: 'Auth completion failed' },
|
|
516
|
+
status: 500 as const,
|
|
517
|
+
};
|
|
518
|
+
} catch (err) {
|
|
519
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
520
|
+
return {
|
|
521
|
+
ok: false as const,
|
|
522
|
+
body: { ok: false, error: msg },
|
|
523
|
+
status: 500 as const,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function getMCPAuthStatus(options: {
|
|
529
|
+
name: string;
|
|
530
|
+
projectRoot?: string;
|
|
531
|
+
oAuthStore: OAuthCredentialStore;
|
|
532
|
+
}) {
|
|
533
|
+
const { name, oAuthStore } = options;
|
|
534
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
535
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
536
|
+
const serverConfig = config.servers.find((server) => server.name === name);
|
|
537
|
+
|
|
538
|
+
if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
|
|
539
|
+
try {
|
|
540
|
+
const auth = await getStoredCopilotMCPToken(
|
|
541
|
+
oAuthStore,
|
|
542
|
+
name,
|
|
543
|
+
serverConfig.scope ?? 'global',
|
|
544
|
+
projectRoot,
|
|
545
|
+
);
|
|
546
|
+
const authenticated = !!auth.token && !auth.needsReauth;
|
|
547
|
+
return { authenticated, authType: 'copilot-device' };
|
|
548
|
+
} catch {
|
|
549
|
+
return { authenticated: false, authType: 'copilot-device' };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const manager = getMCPManager();
|
|
554
|
+
if (!manager) {
|
|
555
|
+
return { authenticated: false };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
return await manager.getAuthStatus(name);
|
|
560
|
+
} catch {
|
|
561
|
+
return { authenticated: false };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export async function revokeMCPAuth(options: {
|
|
566
|
+
name: string;
|
|
567
|
+
projectRoot?: string;
|
|
568
|
+
oAuthStore: OAuthCredentialStore;
|
|
569
|
+
}) {
|
|
570
|
+
const { name, oAuthStore } = options;
|
|
571
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
572
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
573
|
+
const serverConfig = config.servers.find((server) => server.name === name);
|
|
574
|
+
|
|
575
|
+
if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
|
|
576
|
+
try {
|
|
577
|
+
const key = getCopilotMCPOAuthKey(
|
|
578
|
+
name,
|
|
579
|
+
serverConfig.scope ?? 'global',
|
|
580
|
+
projectRoot,
|
|
581
|
+
);
|
|
582
|
+
await oAuthStore.clearServer(key);
|
|
583
|
+
if (key !== name) {
|
|
584
|
+
await oAuthStore.clearServer(name);
|
|
585
|
+
}
|
|
586
|
+
const manager = getMCPManager();
|
|
587
|
+
if (manager) {
|
|
588
|
+
await manager.clearAuthData(
|
|
589
|
+
name,
|
|
590
|
+
serverConfig.scope ?? 'global',
|
|
591
|
+
projectRoot,
|
|
592
|
+
);
|
|
593
|
+
await manager.stopServer(name);
|
|
594
|
+
}
|
|
595
|
+
return { ok: true as const, body: { ok: true, name } };
|
|
596
|
+
} catch (err) {
|
|
597
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
598
|
+
return {
|
|
599
|
+
ok: false as const,
|
|
600
|
+
body: { ok: false, error: msg },
|
|
601
|
+
status: 500 as const,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const manager = getMCPManager();
|
|
607
|
+
if (!manager) {
|
|
608
|
+
return {
|
|
609
|
+
ok: false as const,
|
|
610
|
+
body: { ok: false, error: 'No MCP manager active' },
|
|
611
|
+
status: 400 as const,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
await manager.revokeAuth(name);
|
|
617
|
+
return { ok: true as const, body: { ok: true, name } };
|
|
618
|
+
} catch (err) {
|
|
619
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
620
|
+
return {
|
|
621
|
+
ok: false as const,
|
|
622
|
+
body: { ok: false, error: msg },
|
|
623
|
+
status: 500 as const,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export async function testMCPServer(name: string, projectRoot = process.cwd()) {
|
|
629
|
+
const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
|
|
630
|
+
const serverConfig = config.servers.find((server) => server.name === name);
|
|
631
|
+
|
|
632
|
+
if (!serverConfig) {
|
|
633
|
+
return {
|
|
634
|
+
ok: false as const,
|
|
635
|
+
body: { ok: false, error: `Server "${name}" not found` },
|
|
636
|
+
status: 404 as const,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const client = new MCPClientWrapper(serverConfig);
|
|
641
|
+
try {
|
|
642
|
+
await client.connect();
|
|
643
|
+
const tools = await client.listTools();
|
|
644
|
+
await client.disconnect();
|
|
645
|
+
return {
|
|
646
|
+
ok: true as const,
|
|
647
|
+
body: {
|
|
648
|
+
ok: true,
|
|
649
|
+
name,
|
|
650
|
+
tools: tools.map((tool) => ({
|
|
651
|
+
name: tool.name,
|
|
652
|
+
description: tool.description,
|
|
653
|
+
})),
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
} catch (err) {
|
|
657
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
658
|
+
return {
|
|
659
|
+
ok: false as const,
|
|
660
|
+
body: { ok: false, error: msg },
|
|
661
|
+
status: 500 as const,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OAuthCredentialStore } from '@ottocode/sdk';
|
|
2
|
+
|
|
3
|
+
export const copilotMCPOAuthStore = new OAuthCredentialStore();
|
|
4
|
+
|
|
5
|
+
export const copilotMCPSessions = new Map<
|
|
6
|
+
string,
|
|
7
|
+
{
|
|
8
|
+
deviceCode: string;
|
|
9
|
+
interval: number;
|
|
10
|
+
serverName: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
>();
|