Phase 2 — MCP Tool Server
Goal
An external agent (or MCP Inspector) can connect over HTTPS, authenticate with a bearer token, and call basic messaging tools that actually hit Telegram. This is the phase where the system stops being “a webhook” and starts being “an MCP server.”
Scope / Deliverables
- Mount
@modelcontextprotocol/sdkstreamable HTTP transport at/mcp - Bearer-token middleware (
MCP_AUTH_TOKEN, constant-time compare) -
mcp/server.ts— register tools; emittools/list+tools/callcorrectly - Tool:
send_message(full Tool Contract including reply markup) - Tool:
send_image(start with{url}source only; defer base64 to Phase 7) - Tool:
edit_message - Tool:
send_buttons - Zod input parsing for every tool; consistent error mapping into MCP JSON-RPC errors
- Telegram error mapper (
telegram/errors.ts) — typed mapping from telegraf errors to tool error codes - Outbound logging: every send →
chat_messagesrow (direction='outbound')
Tool contracts to implement match data-and-contracts.md.
Automated Unit Testing
-
auth.bearer.test.ts—- missing
Authorizationheader → 401 - header present but token mismatched → 401 (constant-time path exercised)
- matching token → middleware passes the request through
- header with leading/trailing whitespace handled correctly
- missing
-
tools/messaging.test.ts— for each ofsend_message,send_image,edit_message,send_buttons:- Zod schema rejects malformed input with JSON-RPC error code
INVALID_INPUT - happy-path call invokes the outbound stub with normalized args
send_imagerejects every shape that isn’t{url: string}in this phase (base64/fileIdrejected withINVALID_INPUT)send_buttonsaccepts both{label, callbackData}and{label, url}button variants; rejects mixed/invalid rows
- Zod schema rejects malformed input with JSON-RPC error code
-
telegram/errors.test.ts— telegraf error → tool error code mapping:400“chat not found” →NOT_FOUND, not retried403“bot was blocked by the user” →USER_BLOCKED_BOTand the user row is markedstatus='blocked'429“Too Many Requests” → respectsretry_afteronce, then surfacesRATE_LIMITED- 5xx → retried once then surfaces
UPSTREAM_FAILURE
-
outbound.archive.test.ts—- successful
send_messagewrites achat_messagesrow withdirection='outbound', kind='text', populatedmessage_id,chat_id,text - failed send (mapped error) does not write an archive row
- successful
-
mcp/server.test.ts—tools/listreturns the four registered tools with correct input schemas;tools/callfor an unknown tool name returns the standard JSON-RPC method-not-found error.
Integration scope (deferred): MCP Inspector connects over HTTPS to a real local server with bearer auth — verified manually in the Definition of Done.
Definition of Done
- MCP Inspector connects to
https://bot.<domain>/mcpwith bearer token. - Calling
send_messagefrom Inspector lands the message in Telegram within 2s. - Calling
send_messageto an unknown chat returnsNOT_FOUNDJSON-RPC error. - Missing bearer token → 401.