Lab Notebook: Building the MCP Submit Suggestion Service
The purpose
I wanted readers and AI agents to be able to suggest improvements to articles without needing a user account, a contact form, or an email client. The constraint was that the interface had to be standard enough that any MCP client could call it, but small enough that it did not become a maintenance burden.
I decided to build a single-tool MCP service. The surface would be tiny, but the backend would enforce real validation, rate limiting, and spam protection.
Why streamable HTTP
MCP transports come in several forms. For a public suggestion box, I needed a transport that:
- does not require the client to be on the same machine,
- works in Claude Desktop, GitHub Copilot, Cursor, Goose, and custom clients,
- does not require authentication configuration from the caller.
I compared the three main options.
Streamable HTTP is stateless. The client sends a JSON-RPC request, the server responds, and the connection closes. There is no long-lived SSE channel to keep alive, no port to expose beyond 443, and no per-client token management. This made it the obvious choice.
Tool contract
I defined a single tool with a minimal argument list:
@mcp.tool()
async def submit_suggestion(
suggestion: str,
article_slug: str = "silent-tactition-keyboard"
) -> dict:
...The constraints are:
suggestionmust be between 5 and 1000 characters.article_slugmust match a known article path.- The return value is a small JSON object with
status,http_status, andmessage.
I deliberately avoided adding fields like name, email, or URL. Those would expand the surface and create privacy and validation problems. A suggestion is just text and an optional article pointer.
Backend protections
I layered four protections on the backend:
- Rate limiting: five submissions per IP address per day. I used a memory store keyed by IP and bucketed by day. This is not perfect behind NAT, but it is enough to slow abuse.
- Input validation: length, slug format, and path-safety checks. The slug is compared against a whitelist of known articles.
- Spam protection: a honeypot field in the JSON-RPC request. Legitimate clients will ignore it; naive bots may fill it.
- Persistence: suggestions are written to a durable store for later review. The API returns immediately after validation; persistence is asynchronous.
Client examples
I documented two calling patterns: Python with the FastMCP client and TypeScript with the official MCP SDK.
The Python client:
from fastmcp.client import Client
async with Client("https://practicalcoder.com/mcp-suggest/mcp") as client:
result = await client.call_tool(
"submit_suggestion",
{
"suggestion": "Great article! Consider adding more examples.",
"article_slug": "silent-tactition-keyboard"
}
)The TypeScript client:
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHttpTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const transport = new StreamableHttpTransport(
'https://practicalcoder.com/mcp-suggest/mcp'
);
const client = new Client({ name: 'suggestion-client', version: '1.0.0' });
await client.connect(transport);
const result = await client.callTool({
name: 'submit_suggestion',
arguments: {
suggestion: 'Please add a comparison with other silent keyboards.',
article_slug: 'silent-tactition-keyboard'
}
});These examples serve two audiences: developers who want to wire the endpoint into their own client, and AI agents that need to know the exact argument shape.
Error design
I kept error responses explicit so an agent can react without guessing.
- Invalid suggestion length →
status: "error",http_status: 400. - Unknown article slug →
status: "error",http_status: 400. - Rate limit exceeded →
status: "error",http_status: 429. - Network failure →
status: "error",http_status: 0.
The http_status: 0 convention is a deliberate signal that the request never reached the server, typically because of DNS or TLS failure. Agents can retry with a small delay.
Lessons
The biggest surprise was how little surface area the service needed. A single tool, two arguments, and a strict validation pipeline turned out to be more useful than a larger API. The MCP contract itself provides the value: any client that speaks the protocol can discover the tool, read its description, and call it correctly.
The service is now live at https://practicalcoder.com/mcp-suggest/mcp. I track submissions in the database and review them during weekly content planning.