From 5b6e8b0efc37ce93fe3471eb5d23636155ccd19a Mon Sep 17 00:00:00 2001 From: TooonyChen <26252739+TooonyChen@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:48:13 +1200 Subject: [PATCH] chore: remove dead v1 MCP code and legacy schema dump - src/mcp.ts: unused v1 JSON-RPC MCP implementation (claimed Basic Auth, old tool names); the live server is src/mcp/server.ts - test/mcp.test.ts: only tested the dead file - db/schema.sql: superseded by migrations/ --- AGENTS.md | 2 +- db/schema.sql | 26 ---- src/mcp.ts | 386 ----------------------------------------------- test/mcp.test.ts | 158 ------------------- 4 files changed, 1 insertion(+), 571 deletions(-) delete mode 100644 db/schema.sql delete mode 100644 src/mcp.ts delete mode 100644 test/mcp.test.ts diff --git a/AGENTS.md b/AGENTS.md index 99ba578..94c94c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ - `src/email/handler.ts` — incoming email pipeline; `src/email/rpcEmail.ts` — RPC-compatible `ForwardableEmailMessage` wrapper. - `src/mcp/server.ts` — remote MCP server at `/mcp` (`@hono/mcp` Streamable HTTP): `list_addresses`, `list_codes`, `get_latest_code`, `wait_for_code`. - `web/` — React 18 + Vite + Tailwind + shadcn/ui frontend. Built to `web/dist`, served via Cloudflare `ASSETS` binding. Pages: Login (doubles as first-admin setup), Inbox, API Keys, Users & Access (admin). -- `migrations/` — D1 schema, applied with `wrangler d1 migrations apply`. Tables: `raw_mails`, `code_mails` (+ `category`), `users`, `api_keys`, `grants`. (`db/schema.sql` is the pre-migration legacy dump — do not use it.) +- `migrations/` — D1 schema, applied with `wrangler d1 migrations apply`. Tables: `raw_mails`, `code_mails` (+ `category`), `users`, `api_keys`, `grants`. ## Commands diff --git a/db/schema.sql b/db/schema.sql deleted file mode 100644 index 942ef5c..0000000 --- a/db/schema.sql +++ /dev/null @@ -1,26 +0,0 @@ -CREATE TABLE IF NOT EXISTS raw_mails ( - id INTEGER PRIMARY KEY, - message_id TEXT UNIQUE, - from_addr TEXT, - to_addr TEXT, - subject TEXT, - raw TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - processed INTEGER DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS code_mails ( - id INTEGER PRIMARY KEY, - message_id TEXT UNIQUE, - from_addr TEXT, - from_org TEXT, - to_addr TEXT, - topic TEXT, - code TEXT, - expires_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (message_id) REFERENCES raw_mails(message_id) -); - -CREATE INDEX idx_raw_message_id ON raw_mails (message_id); -CREATE INDEX idx_code_message_id ON code_mails (message_id); diff --git a/src/mcp.ts b/src/mcp.ts deleted file mode 100644 index a87d760..0000000 --- a/src/mcp.ts +++ /dev/null @@ -1,386 +0,0 @@ -const MCP_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'] as const; -const DEFAULT_HTTP_MCP_PROTOCOL_VERSION = '2025-03-26'; -const MAX_MCP_MAIL_LIMIT = 50; - -const MCP_TOOLS = [ - { - name: 'get_latest_mail', - title: 'Get Latest Mail', - description: 'Return the newest extracted mail with parsed details and decoded message bodies.', - inputSchema: { - type: 'object', - properties: {}, - additionalProperties: false, - }, - }, - { - name: 'get_multiple_mail', - title: 'Get Multiple Mail', - description: `Return the newest extracted mails sorted by createdAt descending. Defaults to 5 and caps at ${MAX_MCP_MAIL_LIMIT}.`, - inputSchema: { - type: 'object', - properties: { - limit: { - type: 'integer', - minimum: 1, - maximum: MAX_MCP_MAIL_LIMIT, - default: 5, - description: `How many latest mails to return. Defaults to 5, maximum ${MAX_MCP_MAIL_LIMIT}.`, - }, - }, - additionalProperties: false, - }, - }, -] as const; - -type SupportedMcpProtocolVersion = (typeof MCP_PROTOCOL_VERSIONS)[number]; -type JsonRpcId = string | number | null; - -export interface McpMailDetail { - id: number; - messageId: string | null; - fromOrg: string | null; - fromAddr: string | null; - toAddr: string | null; - subject: string | null; - topic: string | null; - code: string | null; - createdAt: string | null; - textBody: string | null; - htmlBody: string | null; -} - -interface JsonRpcRequest { - jsonrpc: '2.0'; - id?: JsonRpcId; - method?: string; - params?: unknown; - result?: unknown; - error?: unknown; -} - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -function isSupportedMcpProtocolVersion(value: string): value is SupportedMcpProtocolVersion { - return (MCP_PROTOCOL_VERSIONS as readonly string[]).includes(value); -} - -function getEffectiveMcpProtocolVersion(request: Request): SupportedMcpProtocolVersion { - const headerValue = request.headers.get('MCP-Protocol-Version'); - if (headerValue && isSupportedMcpProtocolVersion(headerValue)) { - return headerValue; - } - return DEFAULT_HTTP_MCP_PROTOCOL_VERSION; -} - -function buildMcpResponseHeaders(protocolVersion?: SupportedMcpProtocolVersion): Record { - const headers: Record = { - 'Cache-Control': 'no-store', - }; - - if (protocolVersion) { - headers['MCP-Protocol-Version'] = protocolVersion; - } - - return headers; -} - -function methodNotAllowedResponse(allowed: string): Response { - return new Response('Method Not Allowed', { - status: 405, - headers: { - Allow: allowed, - 'Cache-Control': 'no-store', - }, - }); -} - -function acceptedMcpNotificationResponse(protocolVersion?: SupportedMcpProtocolVersion): Response { - return new Response(null, { - status: 202, - headers: buildMcpResponseHeaders(protocolVersion), - }); -} - -function makeJsonRpcSuccessResponse(id: JsonRpcId, result: unknown, protocolVersion: SupportedMcpProtocolVersion): Response { - return new Response( - JSON.stringify({ - jsonrpc: '2.0', - id, - result, - }), - { - status: 200, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - ...buildMcpResponseHeaders(protocolVersion), - }, - } - ); -} - -function makeJsonRpcErrorResponse( - id: JsonRpcId, - code: number, - message: string, - protocolVersion: SupportedMcpProtocolVersion, - data?: unknown -): Response { - const errorBody: { - jsonrpc: '2.0'; - id: JsonRpcId; - error: { - code: number; - message: string; - data?: unknown; - }; - } = { - jsonrpc: '2.0', - id, - error: { - code, - message, - }, - }; - - if (data !== undefined) { - errorBody.error.data = data; - } - - return new Response(JSON.stringify(errorBody), { - status: code === -32700 || code === -32600 ? 400 : 200, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - ...buildMcpResponseHeaders(protocolVersion), - }, - }); -} - -function makeMcpToolResult(payload: Record, isError = false) { - return { - content: [ - { - type: 'text', - text: JSON.stringify(payload, null, 2), - }, - ], - structuredContent: payload, - ...(isError ? { isError: true } : {}), - }; -} - -function isAllowedMcpOrigin(request: Request): boolean { - const origin = request.headers.get('Origin'); - if (!origin || origin === 'null') { - return true; - } - - return origin === new URL(request.url).origin; -} - -function parsePositiveInteger(value: unknown): number | null { - if (typeof value === 'number' && Number.isInteger(value) && value > 0) { - return value; - } - - if (typeof value === 'string' && /^\d+$/.test(value)) { - const parsedValue = Number.parseInt(value, 10); - return parsedValue > 0 ? parsedValue : null; - } - - return null; -} - -async function executeMcpTool( - name: string, - args: Record | undefined, - readLatestMailDetails: (limit: number) => Promise -): Promise> { - if (name === 'get_latest_mail') { - const [mail] = await readLatestMailDetails(1); - return { - mail: mail ?? null, - }; - } - - if (name === 'get_multiple_mail') { - let limit = 5; - if (args?.limit !== undefined) { - const parsedLimit = parsePositiveInteger(args.limit); - if (parsedLimit === null) { - throw new Error('limit must be a positive integer.'); - } - limit = Math.min(MAX_MCP_MAIL_LIMIT, parsedLimit); - } - - const mails = await readLatestMailDetails(limit); - return { - limit, - count: mails.length, - mails, - }; - } - - throw new Error(`Tool not found: ${name}`); -} - -export function buildMcpBasicAuthConfigSnippet(mcpUrl: string): string { - return JSON.stringify( - { - mcpServers: { - authInbox: { - url: mcpUrl, - headers: { - Authorization: 'Basic ', - }, - }, - }, - }, - null, - 2 - ); -} - -export async function handleMcpRequest( - request: Request, - readLatestMailDetails: (limit: number) => Promise -): Promise { - if (!isAllowedMcpOrigin(request)) { - return new Response(JSON.stringify({ error: 'Forbidden origin for MCP endpoint.' }), { - status: 403, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'no-store', - }, - }); - } - - if (request.method !== 'POST') { - return methodNotAllowedResponse('POST'); - } - - const headerProtocolVersion = request.headers.get('MCP-Protocol-Version'); - if (headerProtocolVersion && !isSupportedMcpProtocolVersion(headerProtocolVersion)) { - return new Response( - JSON.stringify({ - error: 'Unsupported MCP-Protocol-Version header.', - supported: [...MCP_PROTOCOL_VERSIONS], - }), - { - status: 400, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'Cache-Control': 'no-store', - }, - } - ); - } - - const protocolVersion = getEffectiveMcpProtocolVersion(request); - - let payload: unknown; - try { - payload = await request.json(); - } catch { - return makeJsonRpcErrorResponse(null, -32700, 'Parse error', protocolVersion); - } - - if (Array.isArray(payload) || !isRecord(payload) || payload.jsonrpc !== '2.0') { - return makeJsonRpcErrorResponse(null, -32600, 'Invalid Request', protocolVersion); - } - - const message = payload as unknown as JsonRpcRequest; - if (typeof message.method !== 'string') { - return acceptedMcpNotificationResponse(protocolVersion); - } - - if (message.method === 'notifications/initialized') { - return acceptedMcpNotificationResponse(protocolVersion); - } - - if (message.method === 'ping') { - if (message.id === undefined) { - return acceptedMcpNotificationResponse(protocolVersion); - } - return makeJsonRpcSuccessResponse(message.id, {}, protocolVersion); - } - - if (message.method === 'initialize') { - if (!isRecord(message.params) || typeof message.params.protocolVersion !== 'string') { - return makeJsonRpcErrorResponse(message.id ?? null, -32602, 'initialize requires a protocolVersion.', protocolVersion); - } - - const requestedVersion = message.params.protocolVersion; - if (!isSupportedMcpProtocolVersion(requestedVersion)) { - return makeJsonRpcErrorResponse( - message.id ?? null, - -32602, - 'Unsupported protocol version', - protocolVersion, - { - supported: [...MCP_PROTOCOL_VERSIONS], - requested: requestedVersion, - } - ); - } - - return makeJsonRpcSuccessResponse( - message.id ?? null, - { - protocolVersion: requestedVersion, - capabilities: { - tools: {}, - }, - serverInfo: { - name: 'auth-inbox', - title: 'Auth Inbox MCP', - version: '1.0.0', - }, - instructions: 'This endpoint is protected by HTTP Basic Auth. Keep the Authorization header on every MCP request.', - }, - requestedVersion - ); - } - - if (message.id === undefined) { - return acceptedMcpNotificationResponse(protocolVersion); - } - - if (message.method === 'tools/list') { - return makeJsonRpcSuccessResponse(message.id, { tools: MCP_TOOLS }, protocolVersion); - } - - if (message.method === 'tools/call') { - if (!isRecord(message.params) || typeof message.params.name !== 'string') { - return makeJsonRpcErrorResponse(message.id, -32602, 'tools/call requires a valid tool name.', protocolVersion); - } - - const args = isRecord(message.params.arguments) ? message.params.arguments : undefined; - try { - const toolResult = await executeMcpTool(message.params.name, args, readLatestMailDetails); - return makeJsonRpcSuccessResponse(message.id, makeMcpToolResult(toolResult), protocolVersion); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Tool execution failed.'; - const errorCode = errorMessage.startsWith('Tool not found:') ? -32602 : -32000; - - if (errorCode === -32602) { - return makeJsonRpcErrorResponse(message.id, errorCode, errorMessage, protocolVersion); - } - - return makeJsonRpcSuccessResponse( - message.id, - makeMcpToolResult( - { - error: errorMessage, - }, - true - ), - protocolVersion - ); - } - } - - return makeJsonRpcErrorResponse(message.id, -32601, `Method not found: ${message.method}`, protocolVersion); -} diff --git a/test/mcp.test.ts b/test/mcp.test.ts deleted file mode 100644 index 4e0adec..0000000 --- a/test/mcp.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { buildMcpBasicAuthConfigSnippet, handleMcpRequest, type McpMailDetail } from '../src/mcp'; - -const sampleMails: McpMailDetail[] = [ - { - id: 11, - messageId: 'msg-11', - fromOrg: 'Example App', - fromAddr: 'no-reply@example.com', - toAddr: 'user@example.com', - subject: 'Your verification code', - topic: 'login verification', - code: '123456', - createdAt: '2026-03-17T10:00:00.000Z', - textBody: 'Your code is 123456.', - htmlBody: '

Your code is 123456.

', - }, - { - id: 10, - messageId: 'msg-10', - fromOrg: 'Second App', - fromAddr: 'hello@second.com', - toAddr: 'user@example.com', - subject: 'Magic link', - topic: 'signin link', - code: 'https://second.com/login', - createdAt: '2026-03-17T09:00:00.000Z', - textBody: 'Open https://second.com/login', - htmlBody: 'Sign in', - }, -]; - -async function readLatestMailDetails(limit: number): Promise { - return sampleMails.slice(0, limit); -} - -describe('mcp transport', () => { - it('returns initialize metadata', async () => { - const request = new Request('https://authinbox.test/mcp', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'initialize', - params: { - protocolVersion: '2025-06-18', - capabilities: {}, - clientInfo: { - name: 'vitest', - version: '1.0.0', - }, - }, - }), - }); - - const response = await handleMcpRequest(request, readLatestMailDetails); - const payload = await response.json<{ - result: { - protocolVersion: string; - serverInfo: { name: string }; - }; - }>(); - - expect(response.status).toBe(200); - expect(payload.result.protocolVersion).toBe('2025-06-18'); - expect(payload.result.serverInfo.name).toBe('auth-inbox'); - }); - - it('returns the latest mail via get_latest_mail', async () => { - const request = new Request('https://authinbox.test/mcp', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'MCP-Protocol-Version': '2025-06-18', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 2, - method: 'tools/call', - params: { - name: 'get_latest_mail', - arguments: {}, - }, - }), - }); - - const response = await handleMcpRequest(request, readLatestMailDetails); - const payload = await response.json<{ - result: { - structuredContent: { - mail: McpMailDetail; - }; - }; - }>(); - - expect(response.status).toBe(200); - expect(payload.result.structuredContent.mail.id).toBe(11); - expect(payload.result.structuredContent.mail.code).toBe('123456'); - }); - - it('returns multiple mails with the requested limit', async () => { - const request = new Request('https://authinbox.test/mcp', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'MCP-Protocol-Version': '2025-06-18', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 3, - method: 'tools/call', - params: { - name: 'get_multiple_mail', - arguments: { - limit: 2, - }, - }, - }), - }); - - const response = await handleMcpRequest(request, readLatestMailDetails); - const payload = await response.json<{ - result: { - structuredContent: { - limit: number; - count: number; - mails: McpMailDetail[]; - }; - }; - }>(); - - expect(response.status).toBe(200); - expect(payload.result.structuredContent.limit).toBe(2); - expect(payload.result.structuredContent.count).toBe(2); - expect(payload.result.structuredContent.mails).toHaveLength(2); - }); - - it('builds a config snippet with Authorization header', () => { - const config = JSON.parse( - buildMcpBasicAuthConfigSnippet('https://authinbox.test/mcp') - ) as { - mcpServers: { - authInbox: { - url: string; - headers: { - Authorization: string; - }; - }; - }; - }; - - expect(config.mcpServers.authInbox.url).toBe('https://authinbox.test/mcp'); - expect(config.mcpServers.authInbox.headers.Authorization).toBe('Basic '); - }); -});