Skip to content

Commit a6c10cf

Browse files
committed
fix: support mcp
1 parent b33fe35 commit a6c10cf

3 files changed

Lines changed: 143 additions & 3 deletions

File tree

src/cli/operations/dev/web-ui/__tests__/mcp-proxy.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { IncomingMessage, ServerResponse } from 'http';
44
import { beforeEach, describe, expect, it, vi } from 'vitest';
55

66
function mockReq(_body: string): IncomingMessage {
7-
return {} as IncomingMessage;
7+
return { url: '/api/mcp', headers: { host: 'localhost:8081' } } as unknown as IncomingMessage;
88
}
99

1010
function mockRes(): ServerResponse & { _status: number; _headers: Record<string, string>; _body: string } {

src/cli/operations/dev/web-ui/handlers/mcp-proxy.ts

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { RouteContext } from './route-context.js';
1+
import { ConfigIO } from '../../../../../lib';
2+
import { mcpCallTool, mcpInitSession, mcpListTools } from '../../../../aws/agentcore';
3+
import { resolveInvokeTarget } from '../../../../commands/invoke/resolve';
4+
import { type RouteContext, parseRequestUrl } from './route-context.js';
25
import type { IncomingMessage, ServerResponse } from 'http';
36

47
export async function handleMcpProxy(
@@ -7,6 +10,11 @@ export async function handleMcpProxy(
710
res: ServerResponse,
811
origin?: string
912
): Promise<void> {
13+
const { param } = parseRequestUrl(req);
14+
if (param('target') === 'deployed') {
15+
return handleDeployedMcpProxy(ctx, req, res, origin);
16+
}
17+
1018
ctx.setCorsHeaders(res, origin);
1119

1220
const raw = await ctx.readBody(req);
@@ -80,3 +88,135 @@ export async function handleMcpProxy(
8088
res.writeHead(200, { 'Content-Type': 'application/json' });
8189
res.end(JSON.stringify({ success: true, result, sessionId: responseSessionId }));
8290
}
91+
92+
async function handleDeployedMcpProxy(
93+
ctx: RouteContext,
94+
req: IncomingMessage,
95+
res: ServerResponse,
96+
origin?: string
97+
): Promise<void> {
98+
const { configRoot } = ctx.options;
99+
if (!configRoot) {
100+
ctx.setCorsHeaders(res, origin);
101+
res.writeHead(404, { 'Content-Type': 'application/json' });
102+
res.end(JSON.stringify({ success: false, error: 'No agentcore project found' }));
103+
return;
104+
}
105+
106+
const raw = await ctx.readBody(req);
107+
let parsed: {
108+
agentName?: string;
109+
targetName?: string;
110+
body?: Record<string, unknown>;
111+
sessionId?: string;
112+
};
113+
try {
114+
parsed = JSON.parse(raw) as typeof parsed;
115+
} catch {
116+
ctx.setCorsHeaders(res, origin);
117+
res.writeHead(400, { 'Content-Type': 'application/json' });
118+
res.end(JSON.stringify({ success: false, error: 'Invalid JSON' }));
119+
return;
120+
}
121+
122+
const { agentName, targetName, body, sessionId } = parsed;
123+
124+
if (!body) {
125+
ctx.setCorsHeaders(res, origin);
126+
res.writeHead(400, { 'Content-Type': 'application/json' });
127+
res.end(JSON.stringify({ success: false, error: 'body is required' }));
128+
return;
129+
}
130+
131+
const configIO = new ConfigIO({ baseDir: configRoot });
132+
let project;
133+
let deployedState;
134+
let awsTargets;
135+
try {
136+
project = await configIO.readProjectSpec();
137+
deployedState = await configIO.readDeployedState();
138+
awsTargets = await configIO.readAWSDeploymentTargets();
139+
} catch (err) {
140+
ctx.setCorsHeaders(res, origin);
141+
res.writeHead(500, { 'Content-Type': 'application/json' });
142+
res.end(
143+
JSON.stringify({
144+
success: false,
145+
error: `Failed to load config: ${err instanceof Error ? err.message : String(err)}`,
146+
})
147+
);
148+
return;
149+
}
150+
151+
const resolved = await resolveInvokeTarget({
152+
project,
153+
deployedState,
154+
awsTargets,
155+
agentName,
156+
targetName,
157+
sessionId,
158+
configIO,
159+
});
160+
161+
if (!resolved.success) {
162+
ctx.setCorsHeaders(res, origin);
163+
res.writeHead(400, { 'Content-Type': 'application/json' });
164+
res.end(JSON.stringify({ success: false, error: resolved.error.message }));
165+
return;
166+
}
167+
168+
const mcpOpts = {
169+
region: resolved.region,
170+
runtimeArn: resolved.runtimeArn,
171+
bearerToken: resolved.bearerToken,
172+
mcpSessionId: sessionId,
173+
};
174+
175+
const method = (body as { method?: string }).method;
176+
177+
try {
178+
if (method === 'initialize') {
179+
const mcpSessionId = await mcpInitSession(mcpOpts);
180+
ctx.setCorsHeaders(res, origin);
181+
res.writeHead(200, { 'Content-Type': 'application/json' });
182+
res.end(JSON.stringify({ success: true, result: { jsonrpc: '2.0', result: {} }, sessionId: mcpSessionId }));
183+
} else if (method === 'tools/list') {
184+
const result = await mcpListTools(mcpOpts);
185+
ctx.setCorsHeaders(res, origin);
186+
res.writeHead(200, { 'Content-Type': 'application/json' });
187+
res.end(JSON.stringify({ success: true, result: { jsonrpc: '2.0', result } }));
188+
} else if (method === 'tools/call') {
189+
const params = (body as { params?: { name?: string; arguments?: Record<string, unknown> } }).params;
190+
if (!params?.name) {
191+
ctx.setCorsHeaders(res, origin);
192+
res.writeHead(400, { 'Content-Type': 'application/json' });
193+
res.end(JSON.stringify({ success: false, error: 'tools/call requires params.name' }));
194+
return;
195+
}
196+
const response = await mcpCallTool(mcpOpts, params.name, params.arguments ?? {});
197+
ctx.setCorsHeaders(res, origin);
198+
res.writeHead(200, { 'Content-Type': 'application/json' });
199+
res.end(
200+
JSON.stringify({
201+
success: true,
202+
result: { jsonrpc: '2.0', result: { content: [{ type: 'text', text: response }] } },
203+
})
204+
);
205+
} else {
206+
ctx.setCorsHeaders(res, origin);
207+
res.writeHead(400, { 'Content-Type': 'application/json' });
208+
res.end(JSON.stringify({ success: false, error: `Unsupported MCP method: ${method}` }));
209+
}
210+
} catch (err) {
211+
ctx.setCorsHeaders(res, origin);
212+
if (!res.headersSent) {
213+
res.writeHead(502, { 'Content-Type': 'application/json' });
214+
}
215+
res.end(
216+
JSON.stringify({
217+
success: false,
218+
error: `MCP invoke failed: ${err instanceof Error ? err.message : String(err)}`,
219+
})
220+
);
221+
}
222+
}

src/cli/operations/dev/web-ui/web-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ export class WebUIServer {
342342
await handleStart(ctx, req, res, origin);
343343
} else if (req.method === 'POST' && (req.url === '/invocations' || req.url?.startsWith('/invocations?'))) {
344344
await handleInvocations(ctx, req, res, origin);
345-
} else if (req.method === 'POST' && req.url === '/api/mcp') {
345+
} else if (req.method === 'POST' && (req.url === '/api/mcp' || req.url?.startsWith('/api/mcp?'))) {
346346
await handleMcpProxy(ctx, req, res, origin);
347347
} else if (req.method === 'GET' && req.url?.startsWith('/api/a2a/agent-card')) {
348348
await handleA2AAgentCard(ctx, req, res, origin);

0 commit comments

Comments
 (0)