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/
This commit is contained in:
1 parent
b71375bfa7
commit
5b6e8b0efc
4 files changed
+1
-571
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
-386
@@ -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<string, unknown> {
|
||||
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<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'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<string, unknown>, 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<string, unknown> | undefined,
|
||||
readLatestMailDetails: (limit: number) => Promise<McpMailDetail[]>
|
||||
): Promise<Record<string, unknown>> {
|
||||
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 <base64(username:password)>',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleMcpRequest(
|
||||
request: Request,
|
||||
readLatestMailDetails: (limit: number) => Promise<McpMailDetail[]>
|
||||
): Promise<Response> {
|
||||
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);
|
||||
}
|
||||
@@ -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: '<p>Your code is <strong>123456</strong>.</p>',
|
||||
},
|
||||
{
|
||||
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: '<a href="https://second.com/login">Sign in</a>',
|
||||
},
|
||||
];
|
||||
|
||||
async function readLatestMailDetails(limit: number): Promise<McpMailDetail[]> {
|
||||
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 <base64(username:password)>');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user