air-mcp-server 0.1.0

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 (32) hide show
  1. package/README.md +90 -0
  2. package/bin/air-mcp-http.js +2 -0
  3. package/bin/air-mcp.js +10 -0
  4. package/dist/core/utils/air-home.d.ts +6 -0
  5. package/dist/core/utils/air-home.js +72 -0
  6. package/dist/http.js +12738 -0
  7. package/dist/http.js.map +7 -0
  8. package/dist/index.js +12571 -0
  9. package/dist/index.js.map +7 -0
  10. package/dist/packages/mcp-server/src/config.d.ts +17 -0
  11. package/dist/packages/mcp-server/src/config.js +105 -0
  12. package/dist/packages/mcp-server/src/context.d.ts +10 -0
  13. package/dist/packages/mcp-server/src/context.js +18 -0
  14. package/dist/packages/mcp-server/src/http.d.ts +1 -0
  15. package/dist/packages/mcp-server/src/http.js +189 -0
  16. package/dist/packages/mcp-server/src/index.d.ts +1 -0
  17. package/dist/packages/mcp-server/src/index.js +37 -0
  18. package/dist/packages/mcp-server/src/server.d.ts +18 -0
  19. package/dist/packages/mcp-server/src/server.js +35 -0
  20. package/dist/packages/mcp-server/src/tools/get-session-flow-review.d.ts +2 -0
  21. package/dist/packages/mcp-server/src/tools/get-session-flow-review.js +62 -0
  22. package/dist/packages/mcp-server/src/tools/get-session-generation-context.d.ts +2 -0
  23. package/dist/packages/mcp-server/src/tools/get-session-generation-context.js +118 -0
  24. package/dist/packages/mcp-server/src/tools/index.d.ts +2 -0
  25. package/dist/packages/mcp-server/src/tools/index.js +13 -0
  26. package/dist/packages/mcp-server/src/tools/list-recorded-sessions.d.ts +2 -0
  27. package/dist/packages/mcp-server/src/tools/list-recorded-sessions.js +57 -0
  28. package/dist/packages/mcp-server/src/utils/errors.d.ts +4 -0
  29. package/dist/packages/mcp-server/src/utils/errors.js +15 -0
  30. package/dist/packages/mcp-server/src/utils/privacy.d.ts +8 -0
  31. package/dist/packages/mcp-server/src/utils/privacy.js +15 -0
  32. package/package.json +46 -0
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export declare const ConfigSchema: z.ZodObject<{
3
+ dbPath: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ dbPath: string;
6
+ }, {
7
+ dbPath: string;
8
+ }>;
9
+ export type McpServerConfig = z.infer<typeof ConfigSchema>;
10
+ export declare function resolveDbPath(args: string[], env?: NodeJS.ProcessEnv): string;
11
+ export declare function resolveHttpPort(args: string[], env?: NodeJS.ProcessEnv): number;
12
+ export declare function shouldShowHelp(args: string[]): boolean;
13
+ export declare function formatHelp(transport: 'stdio' | 'http'): string;
14
+ /**
15
+ * Parses simple CLI arguments like --db path/to/air.db
16
+ */
17
+ export declare function parseArgs(args: string[]): McpServerConfig;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.ConfigSchema = void 0;
27
+ exports.resolveDbPath = resolveDbPath;
28
+ exports.resolveHttpPort = resolveHttpPort;
29
+ exports.shouldShowHelp = shouldShowHelp;
30
+ exports.formatHelp = formatHelp;
31
+ exports.parseArgs = parseArgs;
32
+ const zod_1 = require("zod");
33
+ const path = __importStar(require("path"));
34
+ const air_home_1 = require("../../../core/utils/air-home");
35
+ exports.ConfigSchema = zod_1.z.object({
36
+ dbPath: zod_1.z.string().min(1, 'DB path must be provided via --db or AIR_MCP_DB_PATH'),
37
+ });
38
+ const DEFAULT_HTTP_PORT = 3333;
39
+ function readFlag(args, name) {
40
+ for (let i = 0; i < args.length; i++) {
41
+ const arg = args[i];
42
+ if (arg === name && args[i + 1]) {
43
+ return args[i + 1];
44
+ }
45
+ if (arg.startsWith(`${name}=`)) {
46
+ return arg.slice(name.length + 1);
47
+ }
48
+ }
49
+ return undefined;
50
+ }
51
+ function resolveDbPath(args, env = process.env) {
52
+ const dbArg = readFlag(args, '--db');
53
+ const dbPath = dbArg ?? env.AIR_MCP_DB_PATH ?? env.AIR_DB_PATH;
54
+ if (dbPath) {
55
+ return path.resolve(dbPath);
56
+ }
57
+ return (0, air_home_1.getDatabasePath)();
58
+ }
59
+ function resolveHttpPort(args, env = process.env) {
60
+ const portValue = readFlag(args, '--port') ?? env.AIR_MCP_PORT;
61
+ if (!portValue) {
62
+ return DEFAULT_HTTP_PORT;
63
+ }
64
+ const parsed = Number.parseInt(portValue, 10);
65
+ if (!Number.isFinite(parsed) || parsed <= 0) {
66
+ throw new Error(`Invalid port: ${portValue}`);
67
+ }
68
+ return parsed;
69
+ }
70
+ function shouldShowHelp(args) {
71
+ return args.includes('--help') || args.includes('-h');
72
+ }
73
+ function formatHelp(transport) {
74
+ const lines = [
75
+ 'AIR MCP Server',
76
+ '',
77
+ 'Configuration:',
78
+ ' --db <path> Path to AIR SQLite database',
79
+ ' AIR_MCP_DB_PATH Database path environment variable',
80
+ ' AIR_DB_PATH Database path environment variable',
81
+ ' Fallback ~/.air/air-data.db',
82
+ ];
83
+ if (transport === 'http') {
84
+ lines.push(' --port <number> HTTP port for Streamable MCP transport');
85
+ lines.push(' AIR_MCP_PORT HTTP port environment variable');
86
+ }
87
+ lines.push('');
88
+ lines.push('Examples:');
89
+ lines.push(' stdio: node scripts/air-mcp.cjs --db C:/path/to/air-data.db');
90
+ lines.push(' http: node scripts/air-mcp.cjs --transport http --db C:/path/to/air-data.db --port 3333');
91
+ return lines.join('\n');
92
+ }
93
+ /**
94
+ * Parses simple CLI arguments like --db path/to/air.db
95
+ */
96
+ function parseArgs(args) {
97
+ const parsed = {
98
+ dbPath: resolveDbPath(args),
99
+ };
100
+ const result = exports.ConfigSchema.safeParse(parsed);
101
+ if (!result.success) {
102
+ throw new Error(`Invalid configuration: ${result.error.message}`);
103
+ }
104
+ return result.data;
105
+ }
@@ -0,0 +1,10 @@
1
+ import { CodegenService } from '@air/codegen';
2
+ import { McpServerConfig } from './config';
3
+ /**
4
+ * Context holds shared services so tools don't instantiate them repeatedly.
5
+ */
6
+ export interface McpContext {
7
+ config: McpServerConfig;
8
+ getCodegenService(): CodegenService;
9
+ }
10
+ export declare function createContext(config: McpServerConfig): McpContext;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createContext = createContext;
4
+ const codegen_1 = require("@air/codegen");
5
+ function createContext(config) {
6
+ let _codegenService = null;
7
+ return {
8
+ config,
9
+ getCodegenService: () => {
10
+ if (!_codegenService) {
11
+ // Pass the configured DB path down to the CodegenService.
12
+ // It handles opening it natively in query_only mode.
13
+ _codegenService = new codegen_1.CodegenService({ dbPath: config.dbPath });
14
+ }
15
+ return _codegenService;
16
+ }
17
+ };
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_crypto_1 = require("node:crypto");
4
+ const express_js_1 = require("@modelcontextprotocol/sdk/server/express.js");
5
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const config_1 = require("./config");
8
+ const context_1 = require("./context");
9
+ const server_1 = require("./server");
10
+ const HTTP_BIND_HOST = '127.0.0.1';
11
+ const MAX_HTTP_SESSIONS = 10;
12
+ const HTTP_SESSION_IDLE_TTL_MS = 15 * 60 * 1000;
13
+ const HTTP_SESSION_CLEANUP_INTERVAL_MS = 60 * 1000;
14
+ function parseHttpArgs(args) {
15
+ const base = (0, config_1.parseArgs)(args);
16
+ return {
17
+ dbPath: base.dbPath,
18
+ port: (0, config_1.resolveHttpPort)(args),
19
+ };
20
+ }
21
+ async function main() {
22
+ if ((0, config_1.shouldShowHelp)(process.argv.slice(2))) {
23
+ console.error((0, config_1.formatHelp)('http'));
24
+ process.exit(0);
25
+ }
26
+ const config = parseHttpArgs(process.argv.slice(2));
27
+ const context = (0, context_1.createContext)({ dbPath: config.dbPath });
28
+ const app = (0, express_js_1.createMcpExpressApp)();
29
+ const sessions = new Map();
30
+ function getSessionId(req) {
31
+ const headerValue = req.headers['mcp-session-id'];
32
+ return typeof headerValue === 'string' ? headerValue : undefined;
33
+ }
34
+ function sendJsonRpcError(res, statusCode, message) {
35
+ res.status(statusCode).json({
36
+ jsonrpc: '2.0',
37
+ error: {
38
+ code: -32000,
39
+ message,
40
+ },
41
+ id: null,
42
+ });
43
+ }
44
+ function sendInternalServerError(res) {
45
+ res.status(500).json({
46
+ jsonrpc: '2.0',
47
+ error: {
48
+ code: -32603,
49
+ message: 'Internal server error',
50
+ },
51
+ id: null,
52
+ });
53
+ }
54
+ async function cleanupSession(sessionId, reason) {
55
+ const session = sessions.get(sessionId);
56
+ if (!session) {
57
+ return;
58
+ }
59
+ sessions.delete(sessionId);
60
+ try {
61
+ await session.transport.close?.();
62
+ }
63
+ catch (error) {
64
+ console.error('[AIR MCP] Failed to close HTTP transport:', error);
65
+ }
66
+ try {
67
+ await session.server.close?.();
68
+ }
69
+ catch (error) {
70
+ console.error('[AIR MCP] Failed to close MCP server:', error);
71
+ }
72
+ console.error(`[AIR MCP] HTTP session cleaned up: ${sessionId} (${reason})`);
73
+ }
74
+ async function handleExistingSessionRequest(req, res) {
75
+ try {
76
+ const sessionId = getSessionId(req);
77
+ const session = sessionId ? sessions.get(sessionId) : undefined;
78
+ if (!session) {
79
+ res.status(400).send('Invalid or missing session ID');
80
+ return;
81
+ }
82
+ session.lastSeenAt = Date.now();
83
+ await session.transport.handleRequest(req, res);
84
+ }
85
+ catch (error) {
86
+ console.error('[AIR MCP] HTTP existing-session error:', error);
87
+ if (!res.headersSent) {
88
+ sendInternalServerError(res);
89
+ }
90
+ }
91
+ }
92
+ const cleanupTimer = setInterval(() => {
93
+ const now = Date.now();
94
+ for (const [sessionId, session] of sessions.entries()) {
95
+ if (now - session.lastSeenAt > HTTP_SESSION_IDLE_TTL_MS) {
96
+ void cleanupSession(sessionId, 'idle-timeout');
97
+ }
98
+ }
99
+ }, HTTP_SESSION_CLEANUP_INTERVAL_MS);
100
+ cleanupTimer.unref?.();
101
+ app.get('/health', (_req, res) => {
102
+ res.json({
103
+ ok: true,
104
+ server: 'air-mcp-server',
105
+ transport: 'streamable-http',
106
+ });
107
+ });
108
+ const handleMcpRequest = async (req, res) => {
109
+ const sessionId = getSessionId(req);
110
+ try {
111
+ const existingSession = sessionId ? sessions.get(sessionId) : undefined;
112
+ if (existingSession) {
113
+ existingSession.lastSeenAt = Date.now();
114
+ await existingSession.transport.handleRequest(req, res, req.body);
115
+ return;
116
+ }
117
+ if (!existingSession) {
118
+ if (sessionId || !(0, types_js_1.isInitializeRequest)(req.body)) {
119
+ sendJsonRpcError(res, 400, 'Bad Request: No valid session ID provided');
120
+ return;
121
+ }
122
+ if (sessions.size >= MAX_HTTP_SESSIONS) {
123
+ sendJsonRpcError(res, 429, 'Too many active MCP HTTP sessions');
124
+ return;
125
+ }
126
+ const server = (0, server_1.createMcpServer)(context);
127
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
128
+ sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
129
+ onsessioninitialized: (newSessionId) => {
130
+ const now = Date.now();
131
+ sessions.set(newSessionId, {
132
+ id: newSessionId,
133
+ server,
134
+ transport,
135
+ createdAt: now,
136
+ lastSeenAt: now,
137
+ });
138
+ },
139
+ });
140
+ transport.onclose = () => {
141
+ const activeSessionId = transport.sessionId;
142
+ if (activeSessionId) {
143
+ void cleanupSession(activeSessionId, 'transport-close');
144
+ }
145
+ };
146
+ await server.connect(transport);
147
+ await transport.handleRequest(req, res, req.body);
148
+ return;
149
+ }
150
+ }
151
+ catch (error) {
152
+ console.error('[AIR MCP] HTTP transport error:', error);
153
+ if (!res.headersSent) {
154
+ sendInternalServerError(res);
155
+ }
156
+ }
157
+ };
158
+ app.post('/mcp', handleMcpRequest);
159
+ app.get('/mcp', handleExistingSessionRequest);
160
+ app.delete('/mcp', handleExistingSessionRequest);
161
+ const httpServer = app.listen(config.port, HTTP_BIND_HOST, (error) => {
162
+ if (error) {
163
+ console.error('[AIR MCP] Failed to start HTTP server:', error);
164
+ process.exit(1);
165
+ }
166
+ console.error(`[AIR MCP] Streamable HTTP server listening on http://${HTTP_BIND_HOST}:${config.port}/mcp`);
167
+ console.error(`[AIR MCP] Database path: ${config.dbPath}`);
168
+ });
169
+ async function shutdown(reason) {
170
+ clearInterval(cleanupTimer);
171
+ const activeSessionIds = [...sessions.keys()];
172
+ for (const sessionId of activeSessionIds) {
173
+ await cleanupSession(sessionId, reason);
174
+ }
175
+ await new Promise((resolve) => {
176
+ httpServer.close(() => resolve());
177
+ });
178
+ }
179
+ process.on('SIGINT', () => {
180
+ void shutdown('sigint').finally(() => process.exit(0));
181
+ });
182
+ process.on('SIGTERM', () => {
183
+ void shutdown('sigterm').finally(() => process.exit(0));
184
+ });
185
+ }
186
+ main().catch((error) => {
187
+ console.error('[AIR MCP] Fatal HTTP initialization error:', error);
188
+ process.exit(1);
189
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
4
+ const config_1 = require("./config");
5
+ const context_1 = require("./context");
6
+ const server_1 = require("./server");
7
+ async function main() {
8
+ try {
9
+ if ((0, config_1.shouldShowHelp)(process.argv.slice(2))) {
10
+ console.error((0, config_1.formatHelp)('stdio'));
11
+ process.exit(0);
12
+ }
13
+ // 1. Parse arguments (e.g., node dist/index.js --db path/to/air.db)
14
+ const config = (0, config_1.parseArgs)(process.argv.slice(2));
15
+ // 2. Initialize context (CodegenService with read-only DB connection)
16
+ const context = (0, context_1.createContext)(config);
17
+ // 3. Create the MCP Server instance
18
+ const server = (0, server_1.createMcpServer)(context);
19
+ // 4. Wire up the stdio transport
20
+ const transport = new stdio_js_1.StdioServerTransport();
21
+ await server.connect(transport);
22
+ // Provide logging to stderr so it doesn't break the JSON stdio protocol on stdout
23
+ console.error(`[AIR MCP] Server running on stdio`);
24
+ console.error(`[AIR MCP] Database path: ${config.dbPath}`);
25
+ // Graceful shutdown
26
+ process.on('SIGINT', async () => {
27
+ console.error(`[AIR MCP] Shutting down...`);
28
+ await server.close();
29
+ process.exit(0);
30
+ });
31
+ }
32
+ catch (error) {
33
+ console.error(`[AIR MCP] Fatal initialization error:`, error);
34
+ process.exit(1);
35
+ }
36
+ }
37
+ main().catch(console.error);
@@ -0,0 +1,18 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { McpContext } from './context';
3
+ export type AirMcpToolDefinition = {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: Record<string, unknown>;
7
+ };
8
+ export type AirMcpToolResponse = {
9
+ content: Array<{
10
+ type: 'text';
11
+ text: string;
12
+ }>;
13
+ };
14
+ export type AirMcpToolHandler = {
15
+ definition: AirMcpToolDefinition;
16
+ handle: (args: unknown, context: McpContext) => Promise<AirMcpToolResponse>;
17
+ };
18
+ export declare function createMcpServer(context: McpContext): Server;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMcpServer = createMcpServer;
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
6
+ const tools_1 = require("./tools");
7
+ const AIR_MCP_INSTRUCTIONS = 'Use AIR tools first for questions about recorded AIR sessions, AIR flow reviews, or AIR generation context. ' +
8
+ 'Prefer list_recorded_sessions instead of searching files or querying SQLite directly when the user asks to list or find sessions. ' +
9
+ 'After selecting a session, use get_session_flow_review for a human-readable review and get_session_generation_context for structured machine-readable step data. ' +
10
+ 'Do not inspect the AIR database via shell when an AIR MCP tool can answer the request.';
11
+ function createMcpServer(context) {
12
+ const server = new index_js_1.Server({
13
+ name: 'air-mcp-server',
14
+ version: '1.0.0',
15
+ }, {
16
+ capabilities: {
17
+ tools: {},
18
+ },
19
+ instructions: AIR_MCP_INSTRUCTIONS,
20
+ });
21
+ const tools = (0, tools_1.getToolHandlers)();
22
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
23
+ return {
24
+ tools: tools.map(tool => tool.definition),
25
+ };
26
+ });
27
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
28
+ const tool = tools.find(t => t.definition.name === request.params.name);
29
+ if (!tool) {
30
+ throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
31
+ }
32
+ return tool.handle(request.params.arguments ?? {}, context);
33
+ });
34
+ return server;
35
+ }
@@ -0,0 +1,2 @@
1
+ import { AirMcpToolHandler } from '../server';
2
+ export declare const getSessionFlowReviewTool: AirMcpToolHandler;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSessionFlowReviewTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const privacy_1 = require("../utils/privacy");
6
+ const errors_1 = require("../utils/errors");
7
+ const GetSessionFlowReviewArgsSchema = zod_1.z.object({
8
+ sessionId: zod_1.z.string().min(1),
9
+ });
10
+ function getSessionIdFromArgs(args) {
11
+ if (args &&
12
+ typeof args === 'object' &&
13
+ 'sessionId' in args &&
14
+ typeof args.sessionId === 'string') {
15
+ return args.sessionId;
16
+ }
17
+ return 'unknown';
18
+ }
19
+ exports.getSessionFlowReviewTool = {
20
+ definition: {
21
+ name: 'get_session_flow_review',
22
+ description: 'Return a human-readable AIR FlowReview for a recorded session.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ sessionId: {
27
+ type: 'string',
28
+ description: 'The AIR recorded session ID to review.',
29
+ },
30
+ },
31
+ required: ['sessionId'],
32
+ additionalProperties: false,
33
+ },
34
+ },
35
+ handle: async (args, context) => {
36
+ try {
37
+ const parsedArgs = GetSessionFlowReviewArgsSchema.parse(args || {});
38
+ const markdown = context
39
+ .getCodegenService()
40
+ .getFlowReviewMarkdown(parsedArgs.sessionId);
41
+ const wrapped = (0, privacy_1.wrapWithPrivacy)({ markdown });
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text',
46
+ text: JSON.stringify(wrapped, null, 2),
47
+ },
48
+ ],
49
+ };
50
+ }
51
+ catch (error) {
52
+ if (error instanceof zod_1.z.ZodError) {
53
+ throw (0, errors_1.createInvalidArgumentsError)(error.message);
54
+ }
55
+ const errorMessage = error instanceof Error ? error.message : String(error);
56
+ if (errorMessage.includes('Session not found')) {
57
+ throw (0, errors_1.createSessionNotFoundError)(getSessionIdFromArgs(args));
58
+ }
59
+ throw (0, errors_1.createDatabaseUnavailableError)(error instanceof Error ? error.message : 'Unknown database error');
60
+ }
61
+ },
62
+ };
@@ -0,0 +1,2 @@
1
+ import { AirMcpToolHandler } from '../server';
2
+ export declare const getSessionGenerationContextTool: AirMcpToolHandler;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSessionGenerationContextTool = void 0;
4
+ const zod_1 = require("zod");
5
+ const privacy_1 = require("../utils/privacy");
6
+ const errors_1 = require("../utils/errors");
7
+ const GetSessionGenerationContextArgsSchema = zod_1.z.object({
8
+ sessionId: zod_1.z.string().min(1),
9
+ offset: zod_1.z.number().optional(),
10
+ limit: zod_1.z.number().optional(),
11
+ });
12
+ function normalizePagination(args) {
13
+ const rawOffset = args.offset ?? 0;
14
+ const offset = Math.max(Math.floor(rawOffset), 0);
15
+ const rawLimit = args.limit ?? 25;
16
+ const limit = Math.min(Math.max(Math.floor(rawLimit), 1), 100);
17
+ return { offset, limit };
18
+ }
19
+ function getSessionIdFromArgs(args) {
20
+ if (args &&
21
+ typeof args === 'object' &&
22
+ 'sessionId' in args &&
23
+ typeof args.sessionId === 'string') {
24
+ return args.sessionId;
25
+ }
26
+ return 'unknown';
27
+ }
28
+ exports.getSessionGenerationContextTool = {
29
+ definition: {
30
+ name: 'get_session_generation_context',
31
+ description: 'Return a paginated machine-readable AIR GenerationContext for a recorded session.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ sessionId: {
36
+ type: 'string',
37
+ description: 'The AIR recorded session ID.',
38
+ },
39
+ offset: {
40
+ type: 'number',
41
+ description: 'Pagination offset. Default 0.',
42
+ },
43
+ limit: {
44
+ type: 'number',
45
+ description: 'Pagination limit. Default 25, max 100.',
46
+ },
47
+ },
48
+ required: ['sessionId'],
49
+ additionalProperties: false,
50
+ },
51
+ },
52
+ handle: async (args, context) => {
53
+ try {
54
+ const parsedArgs = GetSessionGenerationContextArgsSchema.parse(args || {});
55
+ const { offset, limit } = normalizePagination(parsedArgs);
56
+ const service = context.getCodegenService();
57
+ const generationContext = service.buildGenerationContext(parsedArgs.sessionId);
58
+ const totalSteps = generationContext.steps.length;
59
+ const steps = generationContext.steps.slice(offset, offset + limit);
60
+ const hasMore = offset + limit < totalSteps;
61
+ const resolvedSteps = generationContext.steps.filter(s => s.locatorStatus === 'resolved').length;
62
+ const unresolvedSteps = generationContext.steps.filter(s => s.locatorStatus === 'unresolved').length;
63
+ const notApplicableSteps = generationContext.steps.filter(s => s.locatorStatus === 'not_applicable').length;
64
+ const assertionCount = generationContext.steps.reduce((sum, step) => sum + (step.assertions?.length || 0), 0);
65
+ const ignoredStepsCount = generationContext.ignoredSteps?.length ?? 0;
66
+ const response = {
67
+ schemaVersion: 'air:mcp-generation-context-response:v1',
68
+ generationContextSchemaVersion: generationContext.schemaVersion,
69
+ sessionId: generationContext.sessionId,
70
+ url: generationContext.url,
71
+ recordedAt: generationContext.recordedAt,
72
+ metadata: generationContext.metadata,
73
+ totalSteps,
74
+ offset,
75
+ limit,
76
+ hasMore,
77
+ summary: {
78
+ resolvedSteps,
79
+ unresolvedSteps,
80
+ notApplicableSteps,
81
+ assertionCount,
82
+ ignoredStepsCount,
83
+ },
84
+ steps,
85
+ /**
86
+ * Recorded steps not recommended for replay.
87
+ * The LLM must NOT generate code from these unless explicitly requested.
88
+ * Use for context, diagnostics, and fallback explanation only.
89
+ */
90
+ ignoredSteps: generationContext.ignoredSteps ?? [],
91
+ /**
92
+ * Typed generation rules for the IDE LLM.
93
+ * Follow these rules in addition to any user-provided instructions.
94
+ */
95
+ generationGuidance: generationContext.generationGuidance,
96
+ };
97
+ const wrapped = (0, privacy_1.wrapWithPrivacy)(response);
98
+ return {
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ text: JSON.stringify(wrapped, null, 2),
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ catch (error) {
108
+ if (error instanceof zod_1.z.ZodError) {
109
+ throw (0, errors_1.createInvalidArgumentsError)(error.message);
110
+ }
111
+ const errorMessage = error instanceof Error ? error.message : String(error);
112
+ if (errorMessage.includes('Session not found')) {
113
+ throw (0, errors_1.createSessionNotFoundError)(getSessionIdFromArgs(args));
114
+ }
115
+ throw (0, errors_1.createDatabaseUnavailableError)(error instanceof Error ? error.message : 'Unknown database error');
116
+ }
117
+ },
118
+ };
@@ -0,0 +1,2 @@
1
+ import type { AirMcpToolHandler } from '../server';
2
+ export declare function getToolHandlers(): AirMcpToolHandler[];
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getToolHandlers = getToolHandlers;
4
+ const list_recorded_sessions_1 = require("./list-recorded-sessions");
5
+ const get_session_flow_review_1 = require("./get-session-flow-review");
6
+ const get_session_generation_context_1 = require("./get-session-generation-context");
7
+ function getToolHandlers() {
8
+ return [
9
+ list_recorded_sessions_1.listRecordedSessionsTool,
10
+ get_session_flow_review_1.getSessionFlowReviewTool,
11
+ get_session_generation_context_1.getSessionGenerationContextTool,
12
+ ];
13
+ }
@@ -0,0 +1,2 @@
1
+ import { AirMcpToolHandler } from '../server';
2
+ export declare const listRecordedSessionsTool: AirMcpToolHandler;