@ottocode/server 0.1.206 → 0.1.207

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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/routes/mcp.ts +200 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/server",
3
- "version": "0.1.206",
3
+ "version": "0.1.207",
4
4
  "description": "HTTP API server for ottocode",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -49,8 +49,8 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@ottocode/sdk": "0.1.206",
53
- "@ottocode/database": "0.1.206",
52
+ "@ottocode/sdk": "0.1.207",
53
+ "@ottocode/database": "0.1.207",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.1.8"
package/src/routes/mcp.ts CHANGED
@@ -8,6 +8,39 @@ import {
8
8
  addMCPServerToConfig,
9
9
  removeMCPServerFromConfig,
10
10
  } from '@ottocode/sdk';
11
+ import {
12
+ authorizeCopilot,
13
+ pollForCopilotTokenOnce,
14
+ getAuth,
15
+ setAuth,
16
+ } from '@ottocode/sdk';
17
+
18
+ const GITHUB_COPILOT_HOSTS = [
19
+ 'api.githubcopilot.com',
20
+ 'copilot-proxy.githubusercontent.com',
21
+ ];
22
+
23
+ function isGitHubCopilotUrl(url?: string): boolean {
24
+ if (!url) return false;
25
+ try {
26
+ const parsed = new URL(url);
27
+ return GITHUB_COPILOT_HOSTS.some(
28
+ (h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`),
29
+ );
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ const copilotMCPSessions = new Map<
36
+ string,
37
+ {
38
+ deviceCode: string;
39
+ interval: number;
40
+ serverName: string;
41
+ createdAt: number;
42
+ }
43
+ >();
11
44
 
12
45
  export function registerMCPRoutes(app: Hono) {
13
46
  app.get('/v1/mcp/servers', async (c) => {
@@ -30,6 +63,7 @@ export function registerMCPRoutes(app: Hono) {
30
63
  authRequired: status?.authRequired ?? false,
31
64
  authenticated: status?.authenticated ?? false,
32
65
  scope: s.scope ?? 'global',
66
+ ...(isGitHubCopilotUrl(s.url) ? { authType: 'copilot-device' } : {}),
33
67
  };
34
68
  });
35
69
 
@@ -148,6 +182,37 @@ export function registerMCPRoutes(app: Hono) {
148
182
  const status = (await manager.getStatusAsync()).find(
149
183
  (s) => s.name === name,
150
184
  );
185
+
186
+ if (isGitHubCopilotUrl(serverConfig.url) && !status?.connected) {
187
+ const MCP_SCOPES =
188
+ 'repo read:org read:packages gist notifications read:project security_events';
189
+ const existingAuth = await getAuth('copilot');
190
+ const hasMCPScopes =
191
+ existingAuth?.type === 'oauth' && existingAuth.scopes === MCP_SCOPES;
192
+
193
+ if (!existingAuth || existingAuth.type !== 'oauth' || !hasMCPScopes) {
194
+ const deviceData = await authorizeCopilot({ mcp: true });
195
+ const sessionId = crypto.randomUUID();
196
+ copilotMCPSessions.set(sessionId, {
197
+ deviceCode: deviceData.deviceCode,
198
+ interval: deviceData.interval,
199
+ serverName: name,
200
+ createdAt: Date.now(),
201
+ });
202
+ return c.json({
203
+ ok: true,
204
+ name,
205
+ connected: false,
206
+ authRequired: true,
207
+ authType: 'copilot-device',
208
+ sessionId,
209
+ userCode: deviceData.userCode,
210
+ verificationUri: deviceData.verificationUri,
211
+ interval: deviceData.interval,
212
+ });
213
+ }
214
+ }
215
+
151
216
  return c.json({
152
217
  ok: true,
153
218
  name,
@@ -189,6 +254,48 @@ export function registerMCPRoutes(app: Hono) {
189
254
  return c.json({ ok: false, error: `Server "${name}" not found` }, 404);
190
255
  }
191
256
 
257
+ if (isGitHubCopilotUrl(serverConfig.url)) {
258
+ try {
259
+ const MCP_SCOPES =
260
+ 'repo read:org read:packages gist notifications read:project security_events';
261
+ const existingAuth = await getAuth('copilot');
262
+ if (
263
+ existingAuth?.type === 'oauth' &&
264
+ existingAuth.refresh &&
265
+ existingAuth.scopes === MCP_SCOPES
266
+ ) {
267
+ return c.json({
268
+ ok: true,
269
+ name,
270
+ authType: 'copilot-device',
271
+ authenticated: true,
272
+ message: 'Already authenticated with MCP scopes',
273
+ });
274
+ }
275
+
276
+ const deviceData = await authorizeCopilot({ mcp: true });
277
+ const sessionId = crypto.randomUUID();
278
+ copilotMCPSessions.set(sessionId, {
279
+ deviceCode: deviceData.deviceCode,
280
+ interval: deviceData.interval,
281
+ serverName: name,
282
+ createdAt: Date.now(),
283
+ });
284
+ return c.json({
285
+ ok: true,
286
+ name,
287
+ authType: 'copilot-device',
288
+ sessionId,
289
+ userCode: deviceData.userCode,
290
+ verificationUri: deviceData.verificationUri,
291
+ interval: deviceData.interval,
292
+ });
293
+ } catch (err) {
294
+ const msg = err instanceof Error ? err.message : String(err);
295
+ return c.json({ ok: false, error: msg }, 500);
296
+ }
297
+ }
298
+
192
299
  try {
193
300
  let manager = getMCPManager();
194
301
  if (!manager) {
@@ -216,7 +323,66 @@ export function registerMCPRoutes(app: Hono) {
216
323
  app.post('/v1/mcp/servers/:name/auth/callback', async (c) => {
217
324
  const name = c.req.param('name');
218
325
  const body = await c.req.json();
219
- const { code } = body;
326
+ const { code, sessionId } = body;
327
+
328
+ if (sessionId) {
329
+ const session = copilotMCPSessions.get(sessionId);
330
+ if (!session || session.serverName !== name) {
331
+ return c.json({ ok: false, error: 'Session expired or invalid' }, 400);
332
+ }
333
+ try {
334
+ const result = await pollForCopilotTokenOnce(session.deviceCode);
335
+ if (result.status === 'complete') {
336
+ copilotMCPSessions.delete(sessionId);
337
+ await setAuth(
338
+ 'copilot',
339
+ {
340
+ type: 'oauth',
341
+ refresh: result.accessToken,
342
+ access: result.accessToken,
343
+ expires: 0,
344
+ scopes:
345
+ 'repo read:org read:packages gist notifications read:project security_events',
346
+ },
347
+ undefined,
348
+ 'global',
349
+ );
350
+ const projectRoot = process.cwd();
351
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
352
+ const serverConfig = config.servers.find((s) => s.name === name);
353
+ let mcpMgr = getMCPManager();
354
+ if (serverConfig) {
355
+ if (!mcpMgr) {
356
+ mcpMgr = await initializeMCP({ servers: [] }, projectRoot);
357
+ }
358
+ await mcpMgr.restartServer(serverConfig);
359
+ }
360
+ mcpMgr = getMCPManager();
361
+ const status = mcpMgr
362
+ ? (await mcpMgr.getStatusAsync()).find((s) => s.name === name)
363
+ : undefined;
364
+ return c.json({
365
+ ok: true,
366
+ status: 'complete',
367
+ name,
368
+ connected: status?.connected ?? false,
369
+ tools: status?.tools ?? [],
370
+ });
371
+ }
372
+ if (result.status === 'pending') {
373
+ return c.json({ ok: true, status: 'pending' });
374
+ }
375
+ copilotMCPSessions.delete(sessionId);
376
+ return c.json({
377
+ ok: false,
378
+ status: 'error',
379
+ error: result.status === 'error' ? result.error : 'Unknown error',
380
+ });
381
+ } catch (err) {
382
+ const msg = err instanceof Error ? err.message : String(err);
383
+ return c.json({ ok: false, error: msg }, 500);
384
+ }
385
+ }
220
386
 
221
387
  if (!code) {
222
388
  return c.json({ ok: false, error: 'code is required' }, 400);
@@ -249,8 +415,21 @@ export function registerMCPRoutes(app: Hono) {
249
415
 
250
416
  app.get('/v1/mcp/servers/:name/auth/status', async (c) => {
251
417
  const name = c.req.param('name');
252
- const manager = getMCPManager();
418
+ const projectRoot = process.cwd();
419
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
420
+ const serverConfig = config.servers.find((s) => s.name === name);
421
+
422
+ if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
423
+ try {
424
+ const auth = await getAuth('copilot');
425
+ const authenticated = auth?.type === 'oauth' && !!auth.refresh;
426
+ return c.json({ authenticated, authType: 'copilot-device' });
427
+ } catch {
428
+ return c.json({ authenticated: false, authType: 'copilot-device' });
429
+ }
430
+ }
253
431
 
432
+ const manager = getMCPManager();
254
433
  if (!manager) {
255
434
  return c.json({ authenticated: false });
256
435
  }
@@ -265,8 +444,26 @@ export function registerMCPRoutes(app: Hono) {
265
444
 
266
445
  app.delete('/v1/mcp/servers/:name/auth', async (c) => {
267
446
  const name = c.req.param('name');
268
- const manager = getMCPManager();
447
+ const projectRoot = process.cwd();
448
+ const config = await loadMCPConfig(projectRoot, getGlobalConfigDir());
449
+ const serverConfig = config.servers.find((s) => s.name === name);
269
450
 
451
+ if (serverConfig && isGitHubCopilotUrl(serverConfig.url)) {
452
+ try {
453
+ const { removeAuth } = await import('@ottocode/sdk');
454
+ await removeAuth('copilot');
455
+ const manager = getMCPManager();
456
+ if (manager) {
457
+ await manager.stopServer(name);
458
+ }
459
+ return c.json({ ok: true, name });
460
+ } catch (err) {
461
+ const msg = err instanceof Error ? err.message : String(err);
462
+ return c.json({ ok: false, error: msg }, 500);
463
+ }
464
+ }
465
+
466
+ const manager = getMCPManager();
270
467
  if (!manager) {
271
468
  return c.json({ ok: false, error: 'No MCP manager active' }, 400);
272
469
  }