@nospt/plugin-dev-ai-hub-backend 0.2.4 → 0.3.1

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 (46) hide show
  1. package/config.d.ts +22 -0
  2. package/dist/database/TelemetryStore.cjs.js +94 -0
  3. package/dist/database/TelemetryStore.cjs.js.map +1 -0
  4. package/dist/plugin.cjs.js +24 -45
  5. package/dist/plugin.cjs.js.map +1 -1
  6. package/dist/router.cjs.js +147 -202
  7. package/dist/router.cjs.js.map +1 -1
  8. package/dist/service/bodyResolver.cjs.js +56 -0
  9. package/dist/service/bodyResolver.cjs.js.map +1 -0
  10. package/dist/service/telemetryHash.cjs.js +14 -0
  11. package/dist/service/telemetryHash.cjs.js.map +1 -0
  12. package/dist/service/toResourceSummary.cjs.js +31 -0
  13. package/dist/service/toResourceSummary.cjs.js.map +1 -0
  14. package/migrations/001_init.js +13 -42
  15. package/migrations/002_install_count.js +7 -12
  16. package/migrations/003_install_paths.js +7 -14
  17. package/migrations/004_label.js +7 -12
  18. package/migrations/005_remove_unused_fields.js +6 -16
  19. package/migrations/006_resources_content.js +10 -13
  20. package/migrations/007_telemetry.js +28 -0
  21. package/migrations/008_drop_legacy_tables.js +24 -0
  22. package/migrations/009_telemetry_settings.js +25 -0
  23. package/package.json +18 -8
  24. package/.eslintrc.js +0 -1
  25. package/dev/index.ts +0 -8
  26. package/dist/database/AiAssetStore.cjs.js +0 -225
  27. package/dist/database/AiAssetStore.cjs.js.map +0 -1
  28. package/dist/service/AiAssetSyncService.cjs.js +0 -235
  29. package/dist/service/AiAssetSyncService.cjs.js.map +0 -1
  30. package/dist/service/AssetParser.cjs.js +0 -74
  31. package/dist/service/AssetParser.cjs.js.map +0 -1
  32. package/dist/service/McpServerService.cjs.js +0 -366
  33. package/dist/service/McpServerService.cjs.js.map +0 -1
  34. package/package.json-prepack +0 -71
  35. package/src/database/AiAssetStore.test.ts +0 -294
  36. package/src/database/AiAssetStore.ts +0 -256
  37. package/src/index.ts +0 -2
  38. package/src/plugin.ts +0 -84
  39. package/src/router.smoke.test.ts +0 -262
  40. package/src/router.ts +0 -338
  41. package/src/service/AiAssetSyncService.ts +0 -298
  42. package/src/service/AssetParser.test.ts +0 -329
  43. package/src/service/AssetParser.ts +0 -103
  44. package/src/service/McpServerService.ts +0 -464
  45. package/src/types.ts +0 -31
  46. package/tsconfig.json +0 -8
@@ -1,262 +0,0 @@
1
- /**
2
- * Smoke tests for the Express router created by `createRouter`.
3
- *
4
- * These tests verify that all route patterns are correctly resolved by
5
- * path-to-regexp (via Express), that :id parameters are captured, and that the
6
- * right status codes are returned — without starting a real Backstage backend.
7
- */
8
-
9
- import express from 'express';
10
- import request from 'supertest';
11
- import { createRouter } from './router';
12
- import type { AiAssetStore } from './database/AiAssetStore';
13
- import type { AiAssetSyncService } from './service/AiAssetSyncService';
14
- import type { LoggerService } from '@backstage/backend-plugin-api';
15
- import type { ProviderConfig } from './types';
16
-
17
- // ── Fixtures ─────────────────────────────────────────────────────────────────
18
-
19
- const ASSET_SUMMARY = {
20
- id: 'asset-123',
21
- providerId: 'provider-1',
22
- name: 'Test Instruction',
23
- description: 'A test asset',
24
- type: 'instruction',
25
- tools: ['github-copilot'],
26
- tags: ['test'],
27
- author: 'tester',
28
- version: '1.0.0',
29
- installCount: 5,
30
- syncedAt: '2026-01-01T00:00:00Z',
31
- createdAt: '2026-01-01T00:00:00Z',
32
- updatedAt: '2026-01-01T00:00:00Z',
33
- } as const;
34
-
35
- const ASSET_FULL = {
36
- ...ASSET_SUMMARY,
37
- content: '# Test Instruction\n\nSome content.',
38
- yamlRaw: 'name: test',
39
- yamlPath: 'instructions/test.yaml',
40
- mdPath: 'instructions/test.md',
41
- repoUrl: 'https://github.com/org/repo',
42
- branch: 'main',
43
- resourcesContent: null,
44
- installPaths: null,
45
- installPath: null,
46
- icon: null,
47
- label: null,
48
- metadata: null,
49
- };
50
-
51
- const PROVIDER: ProviderConfig = {
52
- id: 'provider-1',
53
- type: 'github',
54
- target: 'https://github.com/org/repo',
55
- branch: 'main',
56
- schedule: {
57
- frequency: { hours: 1 },
58
- timeout: { minutes: 5 },
59
- },
60
- };
61
-
62
- // ── Mocks ────────────────────────────────────────────────────────────────────
63
-
64
- function makeStoreMock(overrides: Partial<AiAssetStore> = {}): AiAssetStore {
65
- return {
66
- listAssets: jest.fn().mockResolvedValue({ items: [ASSET_SUMMARY], totalCount: 1 }),
67
- getAsset: jest.fn().mockImplementation(async (id: string) =>
68
- id === 'asset-123' ? ASSET_FULL : null,
69
- ),
70
- incrementInstallCount: jest.fn().mockResolvedValue(undefined),
71
- getAllSyncStatuses: jest.fn().mockResolvedValue([
72
- { providerId: 'provider-1', status: 'idle', assetCount: 1 },
73
- ]),
74
- getSyncStatus: jest.fn().mockImplementation(async (id: string) =>
75
- id === 'provider-1' ? { providerId: 'provider-1', status: 'idle', assetCount: 1 } : null,
76
- ),
77
- getStats: jest.fn().mockResolvedValue({
78
- totalAssets: 1,
79
- byType: { instruction: 1, agent: 0, skill: 0, workflow: 0 },
80
- byTool: { 'github-copilot': 1 },
81
- byProvider: { 'provider-1': 1 },
82
- }),
83
- // remaining methods not needed by router
84
- upsertAsset: jest.fn(),
85
- deleteAssetsNotIn: jest.fn(),
86
- upsertSyncStatus: jest.fn(),
87
- ...overrides,
88
- } as unknown as AiAssetStore;
89
- }
90
-
91
- function makeSyncServiceMock(): AiAssetSyncService {
92
- return {
93
- syncProvider: jest.fn().mockResolvedValue(undefined),
94
- start: jest.fn().mockResolvedValue(undefined),
95
- } as unknown as AiAssetSyncService;
96
- }
97
-
98
- const noopLogger: LoggerService = {
99
- error: jest.fn(),
100
- warn: jest.fn(),
101
- info: jest.fn(),
102
- debug: jest.fn(),
103
- child: jest.fn().mockReturnThis(),
104
- };
105
-
106
- // ── Test app factory ──────────────────────────────────────────────────────────
107
-
108
- function makeApp(storeOverrides: Partial<AiAssetStore> = {}) {
109
- const store = makeStoreMock(storeOverrides);
110
- const syncService = makeSyncServiceMock();
111
-
112
- const router = createRouter({
113
- logger: noopLogger,
114
- store,
115
- syncService,
116
- providers: [PROVIDER],
117
- });
118
-
119
- const app = express();
120
- app.use(express.json());
121
- app.use(router);
122
- return { app, store, syncService };
123
- }
124
-
125
- // ── Tests ─────────────────────────────────────────────────────────────────────
126
-
127
- describe('router smoke tests (path-to-regexp route matching)', () => {
128
- // ── /assets ────────────────────────────────────────────────────────────────
129
-
130
- describe('GET /assets', () => {
131
- it('returns 200 with paginated asset list', async () => {
132
- const { app } = makeApp();
133
- const res = await request(app).get('/assets');
134
- expect(res.status).toBe(200);
135
- expect(res.body).toMatchObject({ items: expect.any(Array), totalCount: 1 });
136
- });
137
-
138
- it('forwards query parameters to the store', async () => {
139
- const { app, store } = makeApp();
140
- await request(app).get('/assets?type=instruction&search=test&page=2&pageSize=5');
141
- expect(store.listAssets).toHaveBeenCalledWith(
142
- expect.objectContaining({ type: 'instruction', search: 'test', page: 2, pageSize: 5 }),
143
- );
144
- });
145
- });
146
-
147
- describe('GET /assets/:id', () => {
148
- it('returns 200 for a known asset and captures the :id param', async () => {
149
- const { app, store } = makeApp();
150
- const res = await request(app).get('/assets/asset-123');
151
- expect(res.status).toBe(200);
152
- expect(store.getAsset).toHaveBeenCalledWith('asset-123');
153
- expect(res.body.id).toBe('asset-123');
154
- });
155
-
156
- it('returns 404 for an unknown asset', async () => {
157
- const { app } = makeApp();
158
- const res = await request(app).get('/assets/does-not-exist');
159
- expect(res.status).toBe(404);
160
- expect(res.body.error).toMatch(/not found/i);
161
- });
162
- });
163
-
164
- describe('GET /assets/:id/raw', () => {
165
- it('returns 200 with text/markdown content type', async () => {
166
- const { app } = makeApp();
167
- const res = await request(app).get('/assets/asset-123/raw');
168
- expect(res.status).toBe(200);
169
- expect(res.headers['content-type']).toMatch(/text\/markdown/);
170
- });
171
-
172
- it('returns 404 when asset does not exist', async () => {
173
- const { app } = makeApp();
174
- const res = await request(app).get('/assets/missing/raw');
175
- expect(res.status).toBe(404);
176
- });
177
- });
178
-
179
- describe('GET /assets/:id/download', () => {
180
- it('returns 200 with Content-Disposition header for non-skill assets', async () => {
181
- const { app } = makeApp();
182
- const res = await request(app).get('/assets/asset-123/download');
183
- expect(res.status).toBe(200);
184
- expect(res.headers['content-disposition']).toMatch(/attachment/);
185
- expect(res.headers['content-disposition']).toMatch(/\.md/);
186
- });
187
-
188
- it('returns 404 when asset does not exist', async () => {
189
- const { app } = makeApp();
190
- const res = await request(app).get('/assets/missing/download');
191
- expect(res.status).toBe(404);
192
- });
193
- });
194
-
195
- describe('POST /assets/:id/track-install', () => {
196
- it('returns 204 and increments count for known asset', async () => {
197
- const { app, store } = makeApp();
198
- const res = await request(app).post('/assets/asset-123/track-install');
199
- expect(res.status).toBe(204);
200
- expect(store.incrementInstallCount).toHaveBeenCalledWith('asset-123');
201
- });
202
-
203
- it('returns 404 for unknown asset', async () => {
204
- const { app } = makeApp();
205
- const res = await request(app).post('/assets/missing/track-install');
206
- expect(res.status).toBe(404);
207
- });
208
- });
209
-
210
- // ── /providers ─────────────────────────────────────────────────────────────
211
-
212
- describe('GET /providers', () => {
213
- it('returns 200 with provider list', async () => {
214
- const { app } = makeApp();
215
- const res = await request(app).get('/providers');
216
- expect(res.status).toBe(200);
217
- expect(Array.isArray(res.body)).toBe(true);
218
- expect(res.body[0]).toMatchObject({ id: 'provider-1' });
219
- });
220
- });
221
-
222
- describe('GET /providers/:id/status', () => {
223
- it('returns 200 for a known provider and captures :id param', async () => {
224
- const { app, store } = makeApp();
225
- const res = await request(app).get('/providers/provider-1/status');
226
- expect(res.status).toBe(200);
227
- expect(store.getSyncStatus).toHaveBeenCalledWith('provider-1');
228
- });
229
-
230
- it('returns 404 for an unknown provider', async () => {
231
- const { app } = makeApp();
232
- const res = await request(app).get('/providers/unknown-provider/status');
233
- expect(res.status).toBe(404);
234
- });
235
- });
236
-
237
- describe('POST /providers/:id/sync', () => {
238
- it('returns 200 and fires background sync for known provider', async () => {
239
- const { app } = makeApp();
240
- const res = await request(app).post('/providers/provider-1/sync');
241
- expect(res.status).toBe(200);
242
- expect(res.body.providerId).toBe('provider-1');
243
- });
244
-
245
- it('returns 404 for an unknown provider', async () => {
246
- const { app } = makeApp();
247
- const res = await request(app).post('/providers/unknown-provider/sync');
248
- expect(res.status).toBe(404);
249
- });
250
- });
251
-
252
- // ── /stats ─────────────────────────────────────────────────────────────────
253
-
254
- describe('GET /stats', () => {
255
- it('returns 200 with stats object', async () => {
256
- const { app } = makeApp();
257
- const res = await request(app).get('/stats');
258
- expect(res.status).toBe(200);
259
- expect(res.body).toMatchObject({ totalAssets: 1 });
260
- });
261
- });
262
- });
package/src/router.ts DELETED
@@ -1,338 +0,0 @@
1
- import { randomUUID } from 'crypto';
2
- import express from 'express';
3
- import archiver from 'archiver';
4
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
- import type { LoggerService } from '@backstage/backend-plugin-api';
6
- import type { AiAssetStore } from './database/AiAssetStore';
7
- import type { AiAssetSyncService } from './service/AiAssetSyncService';
8
- import { createMcpServer } from './service/McpServerService';
9
- import type { ProviderConfig, AssetListFilter } from './types';
10
- import type { AssetType } from '@nospt/plugin-dev-ai-hub-common';
11
-
12
- interface RouterOptions {
13
- logger: LoggerService;
14
- store: AiAssetStore;
15
- syncService: AiAssetSyncService;
16
- providers: ProviderConfig[];
17
- }
18
-
19
- export function createRouter(options: RouterOptions): express.Router {
20
- const { store, syncService, providers } = options;
21
- const router = express.Router();
22
-
23
- /** Active MCP sessions: sessionId → transport */
24
- const mcpSessions = new Map<string, StreamableHTTPServerTransport>();
25
-
26
- router.use(express.json());
27
-
28
- // ── Assets ────────────────────────────────────────────────────────────────
29
-
30
- router.get('/assets', async (req, res) => {
31
- try {
32
- const filter: AssetListFilter = {
33
- type: req.query.type as AssetType | undefined,
34
- tool: req.query.tool as string | undefined,
35
- providerId: req.query.provider as string | undefined,
36
- search: req.query.search as string | undefined,
37
- tags: req.query.tags
38
- ? (req.query.tags as string).split(',').filter(Boolean)
39
- : undefined,
40
- page: req.query.page ? Number(req.query.page) : 1,
41
- pageSize: req.query.pageSize ? Number(req.query.pageSize) : 20,
42
- };
43
-
44
- const { items, totalCount } = await store.listAssets(filter);
45
-
46
- res.json({
47
- items,
48
- totalCount,
49
- page: filter.page,
50
- pageSize: filter.pageSize,
51
- });
52
- } catch (err) {
53
- options.logger.error('GET /assets failed', err as Error);
54
- res.status(500).json({ error: 'Internal server error' });
55
- }
56
- });
57
-
58
- router.get('/assets/:id', async (req, res) => {
59
- try {
60
- const asset = await store.getAsset(req.params.id);
61
- if (!asset) return res.status(404).json({ error: 'Asset not found' });
62
- return res.json(asset);
63
- } catch (err) {
64
- options.logger.error('GET /assets/:id failed', err as Error);
65
- return res.status(500).json({ error: 'Internal server error' });
66
- }
67
- });
68
-
69
- /** Returns the pure markdown content of the asset */
70
- router.get('/assets/:id/raw', async (req, res) => {
71
- try {
72
- const asset = await store.getAsset(req.params.id);
73
- if (!asset) return res.status(404).json({ error: 'Asset not found' });
74
- res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
75
- return res.send(asset.content);
76
- } catch (err) {
77
- options.logger.error('GET /assets/:id/raw failed', err as Error);
78
- return res.status(500).json({ error: 'Internal server error' });
79
- }
80
- });
81
-
82
- /**
83
- * For skills: returns a zip with the markdown + metadata about bundled resources.
84
- * For other types: returns the markdown as a text file.
85
- */
86
- router.get('/assets/:id/download', async (req, res) => {
87
- try {
88
- const asset = await store.getAsset(req.params.id);
89
- if (!asset) return res.status(404).json({ error: 'Asset not found' });
90
-
91
- const filename = `${asset.name.replace(/\s+/g, '-').toLowerCase()}`;
92
-
93
- if (asset.type === 'skill') {
94
- res.setHeader('Content-Type', 'application/zip');
95
- res.setHeader(
96
- 'Content-Disposition',
97
- `attachment; filename="${filename}.zip"`,
98
- );
99
- const archive = archiver('zip');
100
- archive.pipe(res);
101
- archive.append(asset.content, { name: 'SKILL.md' });
102
- if (asset.resourcesContent) {
103
- for (const [resourcePath, resourceContent] of Object.entries(asset.resourcesContent)) {
104
- archive.append(resourceContent, { name: resourcePath });
105
- }
106
- }
107
- await archive.finalize();
108
- } else {
109
- res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
110
- res.setHeader(
111
- 'Content-Disposition',
112
- `attachment; filename="${filename}.md"`,
113
- );
114
- return res.send(asset.content);
115
- }
116
-
117
- return undefined;
118
- } catch (err) {
119
- options.logger.error('GET /assets/:id/download failed', err as Error);
120
- return res.status(500).json({ error: 'Internal server error' });
121
- }
122
- });
123
-
124
- router.post('/assets/:id/track-install', async (req, res) => {
125
- try {
126
- const asset = await store.getAsset(req.params.id);
127
- if (!asset) return res.status(404).json({ error: 'Asset not found' });
128
- await store.incrementInstallCount(req.params.id);
129
- return res.status(204).send();
130
- } catch (err) {
131
- options.logger.error('POST /assets/:id/track-install failed', err as Error);
132
- return res.status(500).json({ error: 'Internal server error' });
133
- }
134
- });
135
-
136
- // ── Providers ─────────────────────────────────────────────────────────────
137
-
138
- router.get('/providers', async (_req, res) => {
139
- try {
140
- const statuses = await store.getAllSyncStatuses();
141
- const statusMap = new Map(statuses.map(s => [s.providerId, s]));
142
-
143
- const result = providers.map(p => {
144
- const status = statusMap.get(p.id);
145
- return {
146
- id: p.id,
147
- type: p.type,
148
- target: p.target,
149
- branch: p.branch,
150
- lastSync: status?.lastSync,
151
- lastCommit: status?.lastCommit,
152
- status: status?.status ?? 'idle',
153
- error: status?.error,
154
- assetCount: status?.assetCount ?? 0,
155
- };
156
- });
157
-
158
- res.json(result);
159
- } catch (err) {
160
- options.logger.error('GET /providers failed', err as Error);
161
- res.status(500).json({ error: 'Internal server error' });
162
- }
163
- });
164
-
165
- router.get('/providers/:id/status', async (req, res) => {
166
- try {
167
- const provider = providers.find(p => p.id === req.params.id);
168
- if (!provider) return res.status(404).json({ error: 'Provider not found' });
169
-
170
- const status = await store.getSyncStatus(req.params.id);
171
- return res.json(
172
- status ?? {
173
- providerId: req.params.id,
174
- status: 'idle',
175
- assetCount: 0,
176
- },
177
- );
178
- } catch (err) {
179
- options.logger.error('GET /providers/:id/status failed', err as Error);
180
- return res.status(500).json({ error: 'Internal server error' });
181
- }
182
- });
183
-
184
- router.post('/providers/:id/sync', async (req, res) => {
185
- try {
186
- const provider = providers.find(p => p.id === req.params.id);
187
- if (!provider) return res.status(404).json({ error: 'Provider not found' });
188
-
189
- // Fire and forget — respond immediately, sync runs in background
190
- setImmediate(() => syncService.syncProvider(provider));
191
-
192
- return res.json({ message: 'Sync triggered', providerId: provider.id });
193
- } catch (err) {
194
- options.logger.error('POST /providers/:id/sync failed', err as Error);
195
- return res.status(500).json({ error: 'Internal server error' });
196
- }
197
- });
198
-
199
- // ── Stats ─────────────────────────────────────────────────────────────────
200
-
201
- router.get('/stats', async (_req, res) => {
202
- try {
203
- const stats = await store.getStats();
204
- res.json(stats);
205
- } catch (err) {
206
- options.logger.error('GET /stats failed', err as Error);
207
- res.status(500).json({ error: 'Internal server error' });
208
- }
209
- });
210
-
211
- // ── MCP — StreamableHTTP transport ────────────────────────────────────────
212
- //
213
- // Clients connect to /mcp?tool=<ai-tool-name> using the MCP HTTP protocol.
214
- // The `tool` query parameter filters assets to those compatible with that
215
- // AI tool. Each session is stateful: the tool filter is captured at
216
- // initialization time and reused for the session lifetime.
217
- //
218
- // Client configuration example (.mcp.json / mcp_settings.json):
219
- //
220
- // {
221
- // "mcpServers": {
222
- // "dev-ai-hub": {
223
- // "type": "http",
224
- // "url": "http://<backstage-host>:7007/api/dev-ai-hub/mcp?tool=claude-code"
225
- // }
226
- // }
227
- // }
228
- //
229
- // Set AI_HUB_TOOL in your environment and substitute it into the URL, or
230
- // hard-code the tool name per workspace.
231
-
232
- /**
233
- * POST /mcp — Initialize a new MCP session or handle an existing one.
234
- * On first request (no mcp-session-id header), a new session is created
235
- * and its ID is returned in the response header.
236
- */
237
- router.post('/mcp', async (req, res) => {
238
- try {
239
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
240
-
241
- if (sessionId) {
242
- const transport = mcpSessions.get(sessionId);
243
- if (!transport) {
244
- res.status(404).json({ error: 'MCP session not found' });
245
- return;
246
- }
247
- await transport.handleRequest(req, res, req.body);
248
- return;
249
- }
250
-
251
- // New session — extract filters from query params or headers
252
- const toolFilter =
253
- (req.query.tool as string | undefined) ??
254
- (req.headers['x-ai-hub-tool'] as string | undefined) ??
255
- '';
256
- const providerFilter =
257
- (req.query.provider as string | undefined) ??
258
- (req.headers['x-ai-hub-provider'] as string | undefined) ??
259
- '';
260
- // proactive defaults to false; pass ?proactive=true to enable check_for_assets + suggest_assets
261
- const proactiveEnabled =
262
- ((req.query.proactive as string | undefined) ??
263
- (req.headers['x-ai-hub-proactive'] as string | undefined) ??
264
- 'false') === 'true';
265
-
266
- const newSessionId = randomUUID();
267
- const transport = new StreamableHTTPServerTransport({
268
- sessionIdGenerator: () => newSessionId,
269
- onsessioninitialized: id => {
270
- mcpSessions.set(id, transport);
271
- options.logger.debug(
272
- `dev-ai-hub MCP: session ${id} opened (tool="${toolFilter || 'all'}", provider="${providerFilter || 'all'}", proactive=${proactiveEnabled})`,
273
- );
274
- },
275
- });
276
-
277
- transport.onclose = () => {
278
- mcpSessions.delete(newSessionId);
279
- options.logger.debug(`dev-ai-hub MCP: session ${newSessionId} closed`);
280
- };
281
-
282
- const server = createMcpServer(store, toolFilter, providerFilter, providers, proactiveEnabled);
283
- await server.connect(transport);
284
-
285
- await transport.handleRequest(req, res, req.body);
286
- } catch (err) {
287
- options.logger.error('POST /mcp failed', err as Error);
288
- if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });
289
- }
290
- });
291
-
292
- /**
293
- * GET /mcp — SSE stream for server-to-client notifications on an existing session.
294
- */
295
- router.get('/mcp', async (req, res) => {
296
- try {
297
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
298
- if (!sessionId) {
299
- res.status(400).json({ error: 'mcp-session-id header required' });
300
- return;
301
- }
302
- const transport = mcpSessions.get(sessionId);
303
- if (!transport) {
304
- res.status(404).json({ error: 'MCP session not found' });
305
- return;
306
- }
307
- await transport.handleRequest(req, res);
308
- } catch (err) {
309
- options.logger.error('GET /mcp failed', err as Error);
310
- if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });
311
- }
312
- });
313
-
314
- /**
315
- * DELETE /mcp — Terminate an existing MCP session.
316
- */
317
- router.delete('/mcp', async (req, res) => {
318
- try {
319
- const sessionId = req.headers['mcp-session-id'] as string | undefined;
320
- if (!sessionId) {
321
- res.status(400).json({ error: 'mcp-session-id header required' });
322
- return;
323
- }
324
- const transport = mcpSessions.get(sessionId);
325
- if (!transport) {
326
- res.status(404).json({ error: 'MCP session not found' });
327
- return;
328
- }
329
- await transport.handleRequest(req, res);
330
- mcpSessions.delete(sessionId);
331
- } catch (err) {
332
- options.logger.error('DELETE /mcp failed', err as Error);
333
- if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });
334
- }
335
- });
336
-
337
- return router;
338
- }