When the technical design for Entiscore started taking shape, the original document proposed resolving external data access through an MCP server running with stdio transport inside a Vercel serverless function. On paper it was straightforward, a clean way to say "we're using MCP" and have it recorded as an architecture decision from the start.
The problem appeared as soon as we started thinking through how that would actually execute.
Why stdio transport doesn't work in a serverless function
The stdio transport in Model Context Protocol is designed for communication between two separate processes, a client that talks to a server through standard input and output, each running independently. A Vercel serverless function doesn't have that option. Each invocation starts, executes, and terminates within a bounded lifecycle, with no room to spin up a second process that communicates via stdio with the first and keeps it alive between requests.
Forcing that pattern into that environment would have meant fighting the infrastructure instead of using it, and with two days of development ahead, that was not a trade-off worth making.
The decision
The solution was to separate the problem into two layers with distinct purposes.
The external data access tools that Entiscore needs, fetching a site's HTML, reading its robots.txt, checking whether an external link responds, were implemented as plain TypeScript functions. Each one follows exactly the input and output contract that MCP defines for its tools. The agent orchestrator invokes them directly, in the same process, with no transport protocol between them. From the outside it looks like a normal function call, but the data contract entering and leaving each function is identical to what it would be if it lived behind a MCP server.
// Tool contract matching MCP's expected shape
export async function fetchSiteHtml(input: { url: string }): Promise<{
content: string;
statusCode: number;
error?: string;
}> {
try {
const response = await fetch(input.url, {
headers: { "User-Agent": "Entiscore/1.0" },
signal: AbortSignal.timeout(10000),
});
const content = await response.text();
return { content, statusCode: response.status };
} catch (err) {
return { content: "", statusCode: 0, error: String(err) };
}
}
That same logic was then exposed a second time as a MCP server, using the official protocol SDK. That server doesn't run in production. It runs during development so that Kiro can connect to it as an MCP client while writing code, seeing the same tools, with the same contract, that the orchestrator will use directly in production.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "entiscore-tools", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "fetch_site_html",
description: "Fetches the HTML content of a given URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "The URL to fetch" },
},
required: ["url"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "fetch_site_html") {
const result = await fetchSiteHtml(request.params.arguments as { url: string });
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
The result is that the project demonstrates MCP in two distinct and complementary ways. The simplified pattern that solves the infrastructure problem in production, and the server that fulfills the protocol's original purpose during development with Kiro.
What this approach produces in practice
The tools are defined once, in TypeScript, with a contract that matches MCP's expectations. The orchestrator uses them directly in the same process during production. Kiro uses them through the MCP server during development. When a tool needs to change, the change happens in one place and both consumers see it.
The development server also meant that Kiro had accurate, live information about what the tools actually returned during code generation, not inferred types or documentation, but runtime behavior. That reduced the number of iterations needed to get the orchestrator calling the tools correctly.
What transfers beyond this specific project
Adopting a protocol doesn't mean replicating its reference implementation without questioning whether the environment supports it as-is. The MCP data contract, the way input and output for each tool is structured, is independent of the transport used to move it from one side to the other.
The contract can be honored, the conceptual interoperability preserved, and a different invocation mechanism chosen that fits the constraints of the environment where the system will run. In Entiscore's case, that meant direct in-process function calls in production and stdio transport only in development, where the constraints that made stdio impractical in production simply don't apply.
Entiscore is available at entiscore.vercel.app. Built with Next.js, TypeScript, Supabase and Claude API for the Kiro powered by AWS hackathon by Código Facilito.

