{"slug": "why-toolgrain-keeps-mcp-tools-separate-from-servers", "title": "Why Toolgrain keeps MCP tools separate from servers", "summary": "A developer built Toolgrain, an MIT-licensed TypeScript toolkit that makes MCP tools standalone definitions bundling a description, Zod input/output schemas, and a handler, which servers then register under client-facing names. Published as @toolgrain/mcp and @toolgrain/node version 0.1.2, it validates both input and declared output at server execution while raw handler calls bypass that validation, and currently supports tool discovery and execution over stdio and independent HTTP POST requests, with sessions, SSE, resources, and prompts out of scope.", "body_md": "I started Toolgrain while building Pushwing and experimenting with Elysia for an MCP integration.\n\nI wanted a TypeScript workflow where I could define a tool independently, get useful types from its input and output schemas, and reuse that definition across servers.\n\nThe approach I was using wasn’t giving me that combination, and the documentation I found didn’t give me a clear path to the structure I wanted. I thought a toolkit built around those needs could be useful.\n\nThe central design decision in Toolgrain is to make a tool a standalone definition. It bundles its description, Zod schemas, and handler. A server registers those definitions under the names exposed to MCP clients, and a transport handles how requests arrive.\n\nHere’s a small inventory example using the published packages. It runs on Node.js 24.20.0 or later within 24.x, with TypeScript strict mode.\n\nIn a new directory:\n\n```\nnpm init -y\nnpm install @toolgrain/mcp@0.1.2 @toolgrain/node@0.1.2 zod@^4.6.6\nnpm install --save-dev typescript @types/node@^24.10.0\n```\n\nSave this as `server.mts`:\n\n``` js\nimport { createServer, tool } from \"@toolgrain/mcp\";\nimport { stdio } from \"@toolgrain/node\";\nimport { z } from \"zod\";\n\nconst inventory = { notebook: 12, pencil: 48 };\n\nconst getStock = tool({\n  description: \"Look up the stock available for a product\",\n  annotations: { readOnlyHint: true },\n  input: z.object({ sku: z.enum([\"notebook\", \"pencil\"]) }),\n  output: z.object({\n    sku: z.string(),\n    quantity: z.number().int().nonnegative(),\n  }),\n  execute: ({ sku }) => ({ sku, quantity: inventory[sku] }),\n});\n\nconst server = createServer({\n  name: \"inventory-demo\",\n  version: \"1.0.0\",\n  tools: { get_stock: getStock },\n});\n\nawait server.listen(stdio());\n```\n\nCompile it:\n\n```\nnpx tsc server.mts --strict --module NodeNext --target ES2022 --types node\n```\n\nThen call the tool with MCP Inspector’s CLI:\n\n```\nnpx --yes @modelcontextprotocol/inspector@2.11.0 --cli \\\n  --protocol-era modern --format json \\\n  --method tools/call --tool-name get_stock \\\n  --tool-args-json '{\"sku\":\"pencil\"}' \\\n  node server.mjs\n```\n\nInspector starts the server, calls the tool, and exits. The response’s `result.structuredContent` contains:\n\n```\n{ \"sku\": \"pencil\", \"quantity\": 48 }\n```\n\nA few details explain the separation:\n\n`getStock` is the TypeScript value; `get_stock` is the registered name that MCP clients discover and call.`sku` type. Server execution validates both input and the declared output.`execute()` calls are raw handler calls. They bypass server validation.\nIn an application, the handler can call an existing service instead of reading the in-memory inventory. Toolgrain also has context providers for application dependencies.\n\nThe current implementation focuses on tool discovery and execution over stdio and independent HTTP POST requests with JSON responses. Sessions, SSE, resources, and prompts are outside its current scope.\n\nThe separation between tool definitions, server execution, and transports came directly from the workflow I wanted while building Pushwing.\n\nThe [Toolgrain source](https://github.com/keshavkatwe/toolgrain) is MIT-licensed. Its [tool authoring guide](https://toolgrain.mintlify.site/tools/overview) documents the validation and execution behavior in more detail.\n\nHow do you organize tool definitions when the same application operation needs to be exposed through more than one MCP server?\n\n*This article was drafted with AI assistance from my description of the project. The example was verified against Toolgrain 0.1.2.*", "url": "https://wpnews.pro/news/why-toolgrain-keeps-mcp-tools-separate-from-servers", "canonical_source": "https://dev.to/keshav_katwe/why-toolgrain-keeps-mcp-tools-separate-from-servers-2o39", "published_at": "2026-10-27 07:32:10+00:00", "updated_at": "2026-10-27 07:42:11.703599+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "ai-tools"], "entities": ["Toolgrain", "Pushwing", "Elysia", "Zod", "MCP Inspector", "Model Context Protocol", "Node.js", "TypeScript"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/why-toolgrain-keeps-mcp-tools-separate-from-servers", "markdown": "https://wpnews.pro/news/why-toolgrain-keeps-mcp-tools-separate-from-servers.md", "text": "https://wpnews.pro/news/why-toolgrain-keeps-mcp-tools-separate-from-servers.txt", "jsonld": "https://wpnews.pro/news/why-toolgrain-keeps-mcp-tools-separate-from-servers.jsonld"}}