notes
by ImTaegan
README.md
# mcp-server-starter
A small, working **Model Context Protocol** server in TypeScript. Two tools, a
resource, a prompt, and an integration test that proves the whole thing runs.
Built to be read. Every file is short, and the comments explain the decisions
that are not obvious from the code, particularly around tool design, which is
where most MCP servers actually go wrong.
Written alongside a full walkthrough: **[How to build an MCP server](https://www.taegan.dev/guides/build-an-mcp-server)**.
## Why this exists
Most MCP examples online still use `server.tool()`, `server.resource()` and
`server.prompt()`. Those are **deprecated** in the current SDK. This repo uses
`registerTool`, `registerResource` and `registerPrompt`, which is what you
should be writing today.
Verified against `@modelcontextprotocol/sdk@1.30.0` on Node 20+.
## Quick start
```bash
git clone https://github.com/ImTaegan/mcp-server-starter
cd mcp-server-starter
npm install
npm run build
npm test
```
`npm test` spins the real server up against the real client over an in-memory
transport and exercises every tool, the resource, and the prompt. If it passes,
the server works.
## Connect it to a client
Build first (`npm run build`), then point your client at `dist/index.js`.
**Claude Desktop** — `claude_desktop_config.json`:
```json
{
"mcpServers": {
"notes": {
"command": "node",
"args": ["/absolute/path/to/mcp-server-starter/dist/index.js"],
"env": { "NOTES_FILE": "/absolute/path/to/notes.json" }
}
}
}
```
**Claude Code** — from anywhere:
```bash
claude mcp add notes -- node /absolute/path/to/mcp-server-starter/dist/index.js
```
Restart the client. You should see a `notes` server with two tools, and
`summarise-notes` as a slash command.
## What is inside
| Primitive | Name | Purpose |
|---|---|---|
| Tool | `search_notes` | Read-only keyword search, with an optional tag filter |
| Tool | `add_note` | Creates a note, annotated as a write |
| Resource | `notes://all` | Every note as markdown, for attaching as context |
| Prompt | `summarise-notes` | A user-invoked digest, surfaced as a slash command |
Notes persist to a JSON file (`NOTES_FILE`, default `./notes.json`) so the
example has no database and nothing to configure. `src/store.ts` is the only
file that touches storage; swap it for a real database without changing the
server.
## The part that actually matters
A tool's `description` is not documentation. It is the prompt the model reads
when deciding whether to call it. Compare:
```ts
// Bad: technically accurate, useless to a model
description: "Searches notes."
// Good: says what it does, when to use it, and what comes back
description:
"Search the user's saved notes by keyword, optionally filtered to a single tag. " +
"Use this before answering any question about what the user has written down, " +
"and before adding a note, to avoid creating a duplicate. " +
"Returns matching notes ordered by relevance, most relevant first."
```
Nearly every "the agent called the wrong tool" problem is a description
problem, not a model problem. The same applies to parameters: every field in
`inputSchema` carries a `.describe()` for the same reason.
Two other things worth copying:
- **Annotations.** `readOnlyHint` and `idempotentHint` tell the host how risky
a call is, which is what lets it auto-approve reads and confirm writes.
- **Never write to stdout.** The stdio transport *is* stdout. One stray
`console.log` corrupts the protocol stream. Log to stderr.
## Layout
```
src/
index.ts stdio wiring, deliberately tiny
server.ts tools, resource, prompt
store.ts JSON persistence, swap for a real database
server.test.ts integration test over an in-memory transport
```
## Licence
MIT. Take it, strip out the notes, keep the shape.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues