MCP Demo
Summary: This server exposes a single arithmetic tool, add, for summing two numbers.
Discover/list the
addtool and its input schema.Call
addwith required numeric argumentsaandb.Receive the sum of the two numbers as the result.
No other tools or capabilities are available in this schema.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP Demoadd 4 and 7"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Explorer
A hands-on project for learning MCP (Model Context Protocol) by building both halves of the protocol ourselves — a server, then a client — one small, understandable step at a time.
This README is also published as a browsable page: MCP Explorer. It mirrors everything here — keep both in sync as the plan changes.
MCP Server
This half of the project is the one built so far: a real MCP server, growing in stages.
Overview
This repo starts as small as an MCP server can possibly be: one file, one tool, no extra layers. From there, it grows in stages — each one adding a single new concept (a second tool, input validation, a real API, error handling) until it looks like something you'd actually use.
The point isn't to rush to a "real" server. It's to build enough small, working versions that each new MCP concept has somewhere to attach itself in your head.
What is MCP?
MCP (Model Context Protocol) is a standard way for an AI assistant to call out to external tools and data, instead of every app inventing its own custom integration. You write an MCP server that exposes a small set of capabilities — mainly tools (functions the assistant can call) — and any MCP-compatible client (Claude Desktop, Claude Code, etc.) can discover and use them the same way. Think of it like a USB port for AI assistants: one common plug, many different devices behind it.
Initial Setup (Stage 1)
Stage 1 is deliberately as small as a working MCP server can be: a server with a single tool that adds two numbers.
Folder structure
Originally this was one flat package (one package.json, one src/, shared by both the server and the custom client). As of 06-monorepo-server-client-split, server and client are fully separate packages in a pnpm-workspaces monorepo (the repo moved from npm to pnpm in branch 09-switch-to-pnpm) — each with its own package.json (so each declares only the dependencies it actually uses — the client never needed zod, for instance) and its own build/ output:
MCP-Explorer/
├── .gitignore
├── README.md
├── package.json # workspace root: no code, just wiring
├── tsconfig.base.json # compiler options shared by every package
└── packages/
├── server/
│ ├── package.json # @mcp-explorer/server — its own deps, build, start
│ ├── tsconfig.json # extends the base config, sets rootDir/outDir
│ └── src/
│ └── index.ts
└── client/
├── package.json # @mcp-explorer/client — its own deps, build, start
├── tsconfig.json
└── src/
└── client.tsHere's why each piece exists — nothing here is optional scaffolding:
pnpm-workspace.yamland rootpackage.json— the workspace file listspackages/*, sopnpm installat the root links the packages together (webdepends on the client asworkspace:*). pnpm is strict: a package can only import what its ownpackage.jsondeclares, so nothing leaks in through hoisting. The rootpackage.jsonpinspackageManagerand holds the scripts. Itsscriptsjust forward to the right package (pnpm buildbuilds both;pnpm start/pnpm clienteach target one workspace), so the top-level commands you already know didn't change.tsconfig.base.json— the compiler options (target,module,strict, etc.) both packages need are identical, so they live once here instead of being copied twice. Each package's owntsconfig.jsonextendsthis and only adds its ownrootDir/outDir.packages/server/andpackages/client/— each is a real, independent package: its ownpackage.json(own name, own dependencies, ownbuild/startscripts) and its ownsrc/→build/pair. Splitting them this way means the client can never accidentally depend on something only the server needs (or vice versa) — the two halves of the protocol stay genuinely decoupled, not just organized into different folders of one package..gitignore— keeps everynode_modules/andbuild/(server's, client's, and the root's) out of version control. Neither belongs in git history; the pattern matches at any depth, so one.gitignorestill covers both packages.
Nothing else exists yet beyond this split — no shared internal package, no test framework. Those would be answers to problems this project doesn't have yet.
Dependencies
Now split per-package, matching what each half of the protocol actually needs:
@modelcontextprotocol/sdk(bothpackages/serverandpackages/client) — the official library that implements the MCP protocol itself (message formats, the server/client objects, the transport). Without it we'd be hand-writing JSON-RPC message handling.zod(packages/serveronly) — a schema library. When we register theaddtool, we usezodto say "this tool takes two numbers,aandb." The SDK uses that schema for two jobs at once: telling MCP clients what shape of input to send, and rejecting bad input at runtime. The client never validates a schema itself, so it never needed this dependency — a concrete example of why the split makes the dependency list honest instead of shared-and-unused.typescript/@types/node(root-leveldevDependencies, dev-only) — the compiler itself, and type definitions for Node's built-in APIs. These live at the root rather than duplicated in both packages, since it's the same build tooling either way, not something specific to the server or the client.
A note on
zodhere: giving theaddtool a schema is not "Stage 2's input validation" — it's just how any MCP tool declares its shape, even the simplest one. Stage 2 (below) builds on top of this with richer validation: a custom rule with its own error message, not just a type.
The one tool: add
src/index.ts does three things, in order:
Create a server —
new McpServer({ name, version }). This is the object a client talks to.Register one tool —
server.registerTool("add", { ...schema... }, handler). The handler receives already-validated{ a, b }and returns their sum as a text result.Connect a transport —
StdioServerTransport. "stdio" means the client talks to this process over its stdin/stdout, rather than over a network port. It's the simplest possible way to run an MCP server: the client just launches the process and starts writing/reading.
That's the whole mental model for Stage 1: server → tool → transport. Everything MCP does at a larger scale is built from these same three pieces.
Stage 2 — Second tool + input validation (this branch)
Stage 2 adds one more tool, divide, specifically to show what validation looks like once "wrong type" isn't the only way a call can be bad.
Why divide, not something else
add only ever needed zod to describe a shape: two numbers, no further rules — any two numbers make a valid call. divide needs a rule on top of that shape: b can be any number except zero. That's exactly the distinction Stage 1's note above was pointing at — a business rule, not just a type.
b: z
.number()
.refine((value) => value !== 0, {
message: 'b must not be zero — division by zero is undefined'
})
.describe('The denominator (must not be zero)').refine() layers a custom check on top of the base z.number() type check, with its own error message — that message is what the caller actually sees when the rule is broken.
What happens on bad input — verified, not assumed
Calling divide with a: 10, b: 0 never reaches the handler function at all. The SDK validates arguments against the schema before your code runs, and returns this instead:
{
"content": [{ "type": "text", "text": "MCP error -32602: Input validation error: Invalid arguments for tool divide: b must not be zero — division by zero is undefined at b" }],
"isError": true
}Two things worth noticing here:
isError: true— this is a tool-level error result, not a JSON-RPC protocol error and not a crash. The server keeps running; the client is just told this particular call failed, with a message tracing straight back to the.refine()message above.The handler's
console.error('[mcp-demo] divide called...')never prints for this call. Validation genuinely happens before your code ever sees the input — you don't need to (and shouldn't) re-checkb !== 0yourself inside the handler.
Try it yourself
Through the Inspector: call divide with a: 10, b: 2 (works, returns 5), then a: 10, b: 0 (rejected, with the message above).
Stage 3 — Connect to a real data source / API (on main)
Stage 3 adds get_weather(latitude, longitude), calling Open-Meteo (free, no API key) for current temperature and wind speed. It's the first tool doing real I/O instead of pure math.
const response = await fetch(url);
if (!response.ok) {
return { isError: true, content: [{ type: 'text', text: `Open-Meteo returned ${response.status} ${response.statusText}` }] };
}The try/catch around the fetch catches network failure (DNS, unreachable host) the same way divide's .refine() catches bad input — as a normal isError: true result, not a crash. Verified both paths directly: a real call against Open-Meteo, and a call against a deliberately unreachable host, which returned Failed to reach Open-Meteo: fetch failed instead of an unhandled rejection.
This is deliberately not Stage 4's job yet — no retries, no timeout, no partial-data handling. Just: don't let the process die when the network doesn't cooperate.
How to Run
pnpm install
pnpm build
pnpm startpnpm build compiles both packages (packages/server/src/index.ts → packages/server/build/index.js, and the client the same way); pnpm start runs the compiled server specifically. (There's also pnpm dev, which just does both in one step.) These are root-level scripts that forward to the right workspace — you never need to cd into packages/server yourself.
On its own, the server will print mcp-demo server running on stdio to stderr and then just sit there — that's expected, not a hang. Keep reading to see why, and how to actually talk to it.
Understanding the Inspector
Why the server does nothing by itself
An MCP server never acts on its own — it only responds when a client sends it a request. Normally that client is a real AI assistant (Claude Desktop, Claude Code, etc.) deciding on its own when to call a tool. Since we don't have one of those wired up yet, we use the MCP Inspector: a small web UI that plays the role of "the client" — except you click the buttons instead of an AI deciding to.
You (clicking buttons) → Inspector → your server (packages/server/build/index.js)Two things worth knowing about what's actually happening:
Your server and its client talk in JSON-RPC — small JSON messages like
{"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}— sent over the server process's stdin, with replies written back over its stdout.The Inspector is what actually launches your server and sends it those messages. The browser page itself doesn't speak MCP at all — it just tells the Inspector (via its own local proxy) what to send, and shows you what comes back.
Step by step: testing add
Build the server (from inside this project folder — the next command uses a relative path):
pnpm buildStart the Inspector, also from inside this folder:
pnpm dlx @modelcontextprotocol/inspector node packages/server/build/index.jsIt prints a URL with a security token, e.g.
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<token>. Open that exact URL. (The token just proves it's really you talking to your own local Inspector — it isn't part of MCP itself.)Confirm Command is
nodeand Arguments ispackages/server/build/index.js, then click Connect.The Tools tab fills in by itself with
add— that's the Inspector automatically callingtools/listright after connecting.Click
add, type numbers intoaandb, click Run Tool — the sum comes back as the result.
One instance per connection — not the same as pnpm start
Every time you click Connect, the Inspector spawns a brand-new copy of your server — a separate running process from any pnpm start / pnpm dev you might already have open elsewhere. Same code, but two independent instances that share nothing: nothing sent to one shows up in the other. If you're testing through the Inspector, the terminal running pnpm start isn't part of that loop at all — you can ignore it.
What you actually need running
To test a tool through the Inspector, this is the complete list — nothing else:
The server is built —
pnpm buildhas run at least once (and again after any edit), sopackages/server/build/index.jsexists and is current.The Inspector is running — from inside this folder:
pnpm dlx @modelcontextprotocol/inspector node packages/server/build/index.js.The token is copied and pasted — when it starts, the Inspector prints a URL like
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=<token>. Copy everything afterTOKEN=and paste it into the Configuration panel's Proxy Session Token field. Command/Arguments should benode/packages/server/build/index.js.You've clicked Connect.
Notice pnpm start / pnpm dev isn't on that list. It's easy to assume "the server" needs to be running separately, but it doesn't — the Inspector builds nothing itself, but as long as packages/server/build/index.js already exists on disk, it's entirely self-sufficient.
Where your own logs show up
The add handler logs each call:
console.error(`[mcp-demo] add called with a=${a}, b=${b}`);This uses console.error (stderr), never console.log (stdout) — stdout is reserved for the JSON-RPC replies themselves, so anything else printed there would corrupt the protocol. The Inspector captures the spawned server's stderr and shows it inside its own UI — not in whatever terminal you happened to launch pnpm dlx @modelcontextprotocol/inspector from.
Connecting from a Real Client (Cursor)
The Inspector is a stand-in for "the client" — useful for testing, but a real AI assistant talks to this server the same way, just deciding on its own when to call a tool instead of you clicking buttons. Cursor is an MCP client out of the box: point it at the compiled server and it does its own tools/list / tools/call.
Add this to Cursor's mcp.json (Cursor Settings → MCP, or ~/.cursor/mcp.json):
{
"mcpServers": {
"mcp-demo": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/MCP-Explorer/packages/server/build/index.js"]
}
}
}The path must point at
packages/server/build/index.js(the compiled output), sopnpm buildneeds to have run at least once — and again after any server change."type": "stdio"matches exactly how the Inspector talks to it: Cursor launches this as a subprocess and exchanges JSON-RPC over its stdin/stdout.
Restart Cursor (or reload its MCP servers) afterward, and add, divide, and get_weather show up as tools the assistant can call on its own.
Streamable HTTP (second entry point)
Since 08-server-http-transport, the tools live in create-server.ts as a createServer() factory, and there are two entry points that connect it to a transport:
index.ts— stdio. OneMcpServerfor the life of the process, spawned by the client (Inspector, Cursor, etc.).pnpm start.http.ts— Streamable HTTP. A standalone process listening onPORT(default4768);POST /mcphandles requests in stateless mode (sessionIdGenerator: undefined), building a freshMcpServerper request.pnpm start:http.
Both serve identical tools — it's one server definition, two ways to run it. @mcp-explorer/client and the web dashboard connect over HTTP (MCP_SERVER_URL, default http://localhost:4768/mcp), so the server has to be running first. Stateless mode is enough while every tool is a single request/response; anything that pushes updates, remembers a client, or resumes needs stateful mode (an Mcp-Session-Id header plus a map of live sessions) — planned for Stage 4 below.
Roadmap
Every stage (and every side exploration) below gets its own git branch. The habit is: branch → build → understand it → merge into main → branch again for the next thing. Nothing moves to main until it's understood, not just working.
Branch numbering: starting from this point, every new branch — a numbered stage or a side exploration — also gets a global sequence number prefix (
NN-description), so the branch list alone shows true creation order, not just which stages happen to be numbered.stage-1-basic-serverandexplore-mcp-clientpredate this convention (they'd be #1 and #2) and keep their original names. Branch #3 turned out to be a side exploration too (03-claude-md-constitution, addingCLAUDE.md) rather than Stage 2 — proof the counter really is global and doesn't reserve numbers for stages in advance. Stage 2 landed as04-stage-2-second-tool-validation, Stage 3 as05-stage-3-real-data-source, #6 was another side exploration (06-monorepo-server-client-split, splitting server/client into separate npm-workspace packages), and #7 (07-web-nextjs-react, rewriting the web dashboard as Next.js + React) followed it, and #8 (08-server-http-transport, adding a Streamable HTTP entry point to the server and moving the client onto it) came after that, and #9 (09-switch-to-pnpm, moving the repo from npm to pnpm) followed — anything after that gets whatever number comes next when it's actually created.
flowchart LR
S1["Stage 1<br/>Basic server<br/>+ one tool"] --> S2["Stage 2<br/>Second tool<br/>+ input validation"]
S2 --> S3["Stage 3<br/>Real data source<br/>/ API"]
S3 --> S4["Stage 4<br/>Error handling, real-time<br/>+ stateful HTTP"]
S4 --> S5["Stage 5<br/>Resources, prompts<br/>+ completions"]
S5 --> S6["Stage 6<br/>Testing + CI"]
S6 --> S7["Stage 7<br/>Authentication"]
S7 --> S8["Stage 8<br/>Authorization<br/>+ hardening"]
S8 --> S9["Stage 9<br/>Persistence<br/>+ scaling"]
S9 --> S10["Stage 10<br/>Observability"]
S10 --> S11["Stage 11<br/>Containerize<br/>+ deploy"]Stages 1–4 build the server; Stages 5–11 are the enterprise track — what separates a demo server from one a team can run in production.
Stage 1 — Basic server + one trivial tool (on
main) A server exists, and it can do exactly one thing (add). Goal: understand server / tool / transport as separate concepts.Stage 2 — Second tool + input validation (on
main) Add a second, slightly less trivial tool, and lean harder onzod— rejecting bad input with clear errors rather than trusting the caller. Goal: see how multiple tools coexist, and what "validation" means beyond just typing. Branch:04-stage-2-second-tool-validationStage 3 — Connect to a real data source / API (on
main) Swap a toy tool for one that does real (async) work — calling a public API or reading real data. Goal: handle async operations and things that can be slow or unavailable. Branch:05-stage-3-real-data-sourceStage 4 — Error handling, a real-time use case & stateful HTTP Harden the server against failures (bad responses, timeouts, partial data) and add something closer to a genuine use case: a tool that pushes progress to the client while it runs. That needs stateful Streamable HTTP — an
Mcp-Session-Idissued oninitialize, a map of live sessions, oneMcpServerper session, andGET/DELETEon/mcpalongsidePOST. Goal: go from "it works when everything goes right" to "it behaves sensibly when it doesn't," and see why sessions exist at all. Branch:0N-stage-4-error-handling-realtime(number assigned when created)Stage 5 — Resources, prompts & completions Tools are one of three server primitives. Add resources (read-only data a client can attach, addressed by URI) and prompts (reusable templates the user picks), plus argument completion. Goal: know which primitive fits which job instead of making everything a tool. Branch:
0N-stage-5-resources-prompts(number assigned when created)Stage 6 — Testing & CI Unit-test tools, integration-test the server through an in-memory transport and through real HTTP, add contract checks on tool schemas, and run it all in GitHub Actions on every PR. Goal: change a server safely before it has real users. Branch:
0N-stage-6-testing-ciStage 7 — Authentication Protect
/mcpper the MCP authorization spec: the server acts as an OAuth 2.1 resource server, validates bearer tokens (JWT signature, issuer, audience, expiry), returns401withWWW-Authenticate, and serves protected-resource metadata so clients can discover the auth server. Goal: know who is calling. Branch:0N-stage-7-authenticationStage 8 — Authorization & security hardening Per-tool permissions from token scopes, per-user/tenant data isolation,
Originvalidation (DNS rebinding), rate limiting, request size limits, output sanitization, secret management, and an audit log of every tool call. Goal: know what each caller may do, and be able to prove what they did. Branch:0N-stage-8-authorization-hardeningStage 9 — Persistence & horizontal scaling Replace the in-memory session map with a shared store (e.g. Redis), add resumable streams (
Last-Event-IDwith an event store), and run several instances behind a load balancer. Goal: survive restarts and scale out without losing sessions. Branch:0N-stage-9-persistence-scalingStage 10 — Observability & operations Structured logs with a request/session id, OpenTelemetry traces across tool calls and upstream APIs, metrics (latency, error rate, active sessions), health/readiness endpoints, timeouts, cancellation, and graceful shutdown. Goal: see what the server is doing in production, and stop it without dropping work. Branch:
0N-stage-10-observabilityStage 11 — Containerize & deploy Dockerfile, environment-based config, a CI/CD pipeline to a real host, a gateway/reverse proxy (TLS, no SSE buffering), and a versioning and deprecation policy for tools. Goal: ship it, update it, and roll it back. Branch:
0N-stage-11-containerize-deploy
Where we are: Stages 1–3 are implemented and merged; Stage 4 hasn't started. Branches #6 (06-monorepo-server-client-split), #7 (07-web-nextjs-react) and #8 (08-server-http-transport, stateless Streamable HTTP on the server, client and web moved onto it) are merged side explorations, and #9 (09-switch-to-pnpm, this branch — moving the repo from npm to pnpm) is the current one, not a numbered stage. Stages 4–11 above are the plan, not a promise of exact detail.
Next Steps
Run
pnpm install && pnpm build, start the HTTP server (pnpm start:http), then confirmadd,divide, andget_weatherwork throughpnpm clientandpnpm web; the Inspector still works over stdio againstpackages/server/build/index.js.Once that makes sense, this branch is ready for a PR into
main.Start Stage 4 (error handling, a real-time use case, stateful HTTP) on the next numbered branch afterward, inside
packages/server.
If anything above didn't need to exist for a tool to work, that's a sign it snuck in ahead of schedule — flag it before moving on.
Related MCP server: Math Addition MCP Server
MCP Client
Everything under MCP Server covers one half of the protocol: a program that answers requests. The other half is a client — something that connects to a server and calls its tools. The Inspector (above) is one example of a client, but it's a pre-built tool with a UI. This side of the project is about writing a minimal client from scratch instead, in plain code, to see that half of the exchange directly.
Status: built, on main. This isn't one of the four numbered server stages — it was a separate, parallel exploration (originally explore-mcp-client, merged via #2) for understanding the client side of MCP, and it's kept up to date as the server grows.
What it does:
packages/client/src/client.tsconnects to the running HTTP server from code (no Inspector, no UI), callslistTools(), thencallTool()against every current tool —add(2, 3),divide(10, 2),divide(10, 0)to show a validation rejection coming back as data (isError: true, not a thrown exception), andget_weather. Run it withpnpm clientfrom the repo root, with the server already running.A detail from the monorepo split (historical): since Stage
06-monorepo-server-client-split, server and client are separate packages with separatebuild/output, so the client can no longer assume the server's compiled file is a sibling of wherever it's run from. It resolves the path itself, relative to its own file location:const serverEntry = path.resolve(here, '../../server/build/index.js');This makes the client correct regardless of the current working directory it's launched from — the same property a real client like Claude Desktop needs, since it launches your server from wherever it happens to run, not from inside this repo.
Another detail, from
07-web-nextjs-react: this resolution moved from module load time intoconnectToServer()itself, and swappedimport.meta.dirnameforpath.dirname(fileURLToPath(import.meta.url)). Next.js's build process imports this module (to collect page data) without necessarily callingconnectToServer(), and in that import context — and again, separately, under Turbopack's externalized-module loader at runtime —import.meta.dirnamecame backundefined. Resolving lazily, inside the function, and viaimport.meta.urlrather than the newerdirnameshorthand, made it work in every context that imports this module, not just the ones that call it eagerly at the top.Since
08-server-http-transport: all of that path resolution is gone.connectToServer(url?)inconnection.tsuses the SDK'sStreamableHTTPClientTransportand a URL (MCP_SERVER_URL, defaulthttp://localhost:4768/mcp) instead of spawning a process, so the client no longer needs to know where the server's build output lives — or even that it's on the same machine. That's what letswebbe deployed separately from the server.Why it's worth doing: it confirms the client/server relationship holds regardless of who's on the client end — a human clicking buttons, or a few lines of TypeScript. It's also a convenient scripted way to re-check every tool at once, without the Inspector's manual clicking.
Web Dashboard
A third client, alongside the Inspector and the scripted client.ts: a browser GUI that discovers the server's tools live and lets you run them by filling in a form — no editing code to try different input.
Status: built and merged (07-web-nextjs-react). Like the client, this is a side exploration, not a numbered server stage. It started (in 06-monorepo-server-client-split) as a plain Express server handing out one static HTML file with vanilla JS, talking to @mcp-explorer/client through two hand-rolled endpoints (GET /api/tools, POST /api/call). This branch rebuilds it as an actual Next.js + React app.
What it does:
packages/web/src/app/page.tsxis an async Server Component — it callsgetClient()andlistTools()directly, on the server, no REST hop in between. Each tool renders as aToolCard(a Client Component), which builds its input fields from the tool's liveinputSchemaand, on "Run", calls a Server Action —runTool()— directly, the same way you'd call any other async function, nofetch/JSON.stringifyin sight. Start the server first (pnpm start:http) —webconnects to it viaMCP_SERVER_URL. Run it withpnpm web(production build) orpnpm web:dev(hot reload) from the repo root, athttp://localhost:4767.Why Server Components + Server Actions, not a REST port: the old
/api/toolsand/api/callendpoints could have been ported to Next.js Route Handlers verbatim — smaller diff, same mental model. Building it the other way instead is the point: Server Components and Server Actions are what's actually different about Next.js, not just "Express with extra steps." No manual endpoints exist for the dashboard's own use at all.The connection is a singleton, again:
packages/web/src/lib/mcp-client.tscaches the in-flight promise fromconnectToServer()onglobalThis, not a module-level variable — the same HTTP connection as the CLI client, reused across every request instead of reconnecting each time.globalThisspecifically (not a plainlet) because Next.js's dev-mode Fast Refresh re-evaluates a module's own scope on every edit; a module-level variable would reset and open a new connection on each reload, whileglobalThissurvives it. The promise (not the resolved client) is cached so two requests racing in before the first connection resolves don't each open their own. (Before08-server-http-transportthis connection spawned a server subprocess, which made the singleton matter more.)page.tsxis forced dynamic (export const dynamic = 'force-dynamic'): without it,next buildwould statically prerender the route — which means actually executing the Server Component, and thus connecting to the MCP server, at build time, then baking that one-time tool list into static HTML instead of fetching it fresh per request.
Available Tools
1 tooladdAdd two numbersB
Adds two numbers together and returns their sum.
| Name | Required | Description | Default |
|---|---|---|---|
| a | Yes | The first number | |
| b | Yes | The second number |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full behavioral burden. It states the operation is pure addition but discloses nothing about return format, numeric precision, integer vs float handling, or overflow behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with zero waste. Appropriately sized for the operation's triviality.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-param arithmetic tool with full schema coverage and no output schema, the description is minimally viable. It could do more on numeric semantics (float precision, edge cases) given no annotations and no output schema.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and both parameters ('a', 'b') are documented in the schema, so the baseline is 3. The description adds nothing beyond the schema, e.g. ordering or numeric type constraints.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Adds) and resource (two numbers) with the outcome (returns their sum). It's unambiguous, though with no siblings there's nothing to differentiate from.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No when-to-use guidance, no exclusions, no alternatives. For a trivial arithmetic tool this is largely self-evident, but the description provides no contextual guidance at all.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v0.1.0- First observed
add
TDQS
Scored across 1 tool
With only a single tool, there is no possibility of confusion or misselection between tools. The purpose (adding two numbers) is unambiguous.
The lone tool 'add' uses a clear, simple verb, but with only one name there is no established convention to be consistent with. No violations exist, but the pattern is untestable.
A single trivial tool is far too thin for any meaningful server scope, even a demo. The surface offers no room for a real workflow.
For an arithmetic domain, only addition is covered; subtraction, multiplication, and division are missing. Agents relying on this surface will hit dead ends for any non-additive task.
Maintenance
Related MCP Connectors
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Related MCP Servers
- FlicenseBqualityNot gradedmaintenanceA minimal Model Context Protocol server that provides a simple tool for adding two integers. It serves as a demonstration of the official Python MCP SDK and is designed for local execution via stdio.1-
- FlicenseNot gradedqualityDmaintenanceA simple MCP server that provides a tool for adding two numbers, designed for testing MCP integration with Claude.-
- FlicenseNot gradedqualityDmaintenanceA model-agnostic MCP server exposing example tools (add1, multiply2, greet) for learning purposes, working with any LLM through stdio transport.-
- AlicenseAqualityCmaintenanceEnables users to run a modular TypeScript MCP server that communicates over STDIO and exposes tools for summing numbers and greeting users.2ISC