/**
* Shopify Agentic MCP Gateway — Entry Point
* Serves as both AWS Lambda handler (HTTP API Gateway) and local stdio MCP server.
*/
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createMCPServer } from './server.js';
import { generateProfile } from './ucp/profile.js';
import { loadConfig } from './types.js';
import { logger } from './utils/logger.js';
import { notFound, toUCPErrorResponse } from './utils/errors.js';
import type { UCPError } from './utils/errors.js';
import { routeUCPv1 } from './rest/router.js';
import { handleMCPRequest, handleMCPOptions } from './mcp/handler.js';
import { handleA2ARequest, handleA2AOptions, handleA2AGetAgentCard } from './a2a/handler.js';
import { validateApiAuth } from './middleware/api-auth.js';
// ─── Lambda Types ───
interface APIGatewayEvent {
httpMethod: string;
path: string;
headers: Record<string, string | undefined>;
body: string | null;
isBase64Encoded: boolean;
requestContext: Record<string, unknown>;
queryStringParameters: Record<string, string | undefined> | null;
pathParameters: Record<string, string | undefined> | null;
}
interface LambdaContext {
functionName: string;
awsRequestId: string;
getRemainingTimeInMillis(): number;
}
interface LambdaResponse {
statusCode: number;
headers: Record<string, string>;
body: string;
}
// ─── Response Helpers ───
function getAllowedOrigin(): string {
return process.env['ALLOWED_ORIGIN'] ?? '*';
}
function jsonResponse(statusCode: number, body: unknown): LambdaResponse {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': getAllowedOrigin(),
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, UCP-Agent',
},
body: JSON.stringify(body),
};
}
function errorResponse(error: UCPError): LambdaResponse {
return jsonResponse(error.statusCode, {
error: toUCPErrorResponse(error),
});
}
// ─── Route Handlers ───
function handleWellKnownUCP(): LambdaResponse {
const config = loadConfig();
const profile = generateProfile(config);
logger.info('Serving UCP profile');
return jsonResponse(200, profile);
}
async function handleUCPV1(event: APIGatewayEvent): Promise<LambdaResponse> {
const result = await routeUCPv1(event);
return jsonResponse(result.statusCode, result.body);
}
async function handleMCP(event: APIGatewayEvent): Promise<LambdaResponse> {
const result = await handleMCPRequest(event);
return {
statusCode: result.statusCode,
headers: {
...result.headers,
'Access-Control-Allow-Origin': getAllowedOrigin(),
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Mcp-Session-Id, UCP-Agent',
},
body: result.body,
};
}
// ─── Lambda Handler ───
export async function handler(
event: APIGatewayEvent,
_context: LambdaContext,
): Promise<LambdaResponse> {
const path = event.path;
const method = event.httpMethod.toUpperCase();
logger.info('Lambda invocation', { method, path });
// CORS preflight
if (method === 'OPTIONS') {
if (path === '/mcp') {
const opts = handleMCPOptions();
return { statusCode: opts.statusCode, headers: opts.headers, body: opts.body };
}
if (path === '/a2a') {
const opts = handleA2AOptions();
return { statusCode: opts.statusCode, headers: opts.headers, body: opts.body };
}
return jsonResponse(204, '');
}
// ── Authentication guard ──
const authResult = validateApiAuth(path, event.headers as Record<string, string | undefined>);
if (!authResult.authenticated) {
return jsonResponse(401, {
error: { type: 'error', code: 'unauthorized', message: authResult.error },
});
}
try {
// Route: /.well-known/ucp
if (path === '/.well-known/ucp') {
return handleWellKnownUCP();
}
// Route: /ucp/v1/*
if (path.startsWith('/ucp/v1/')) {
return await handleUCPV1(event);
}
// Route: /mcp
if (path === '/mcp') {
return await handleMCP(event);
}
// Route: /a2a
if (path === '/a2a') {
if (method === 'GET') {
const result = handleA2AGetAgentCard();
return { statusCode: result.statusCode, headers: result.headers, body: result.body };
}
const result = await handleA2ARequest(event);
return { statusCode: result.statusCode, headers: result.headers, body: result.body };
}
// Route: /.well-known/agent.json
if (path === '/.well-known/agent.json') {
const result = handleA2AGetAgentCard();
return { statusCode: result.statusCode, headers: result.headers, body: result.body };
}
// 404 for everything else
return errorResponse(notFound(`No route for ${method} ${path}`));
} catch (err) {
logger.error('Unhandled error in Lambda handler', {
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
return jsonResponse(500, {
error: {
type: 'error',
code: 'internal_error',
message: 'Internal server error',
},
});
}
}
// ─── Local stdio MCP Server ───
async function startStdioServer(): Promise<void> {
logger.info('Starting MCP server in stdio mode (local dev)');
const server = createMCPServer();
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('MCP stdio server running — connected to stdin/stdout');
}
// Auto-start stdio server when not running on Lambda
if (!process.env['AWS_LAMBDA_FUNCTION_NAME']) {
startStdioServer().catch((err: unknown) => {
logger.error('Failed to start stdio MCP server', {
error: err instanceof Error ? err.message : String(err),
});
process.exit(1);
});
}