DevNotes MCP
by Lazaro549
README.md
<div align="center">
# ποΈ DevNotes MCP
**A notes server built to exercise every advanced feature of the Model Context Protocol.**
Follow-up project for Anthropic Academy's *Model Context Protocol: Advanced Topics* course.
<img src="assets/certificate.png" alt="Certificate of completion β Model Context Protocol: Advanced Topics β Lazaro Gomez Vitolo" width="600">
</div>
## π What this is
DevNotes is a small, in-memory notes server β think a minimal Notion, exposed over MCP instead
of a web UI. It's deliberately not trying to be a real product; every tool exists to be a clean,
runnable example of one specific advanced MCP feature, and the notes themselves are just the
excuse to have something worth calling a tool *on*.
| Course topic | Where it lives |
|---|---|
| **Sampling** β server asks the client's LLM to do the work | [`summarize-note`](src/server/tools/summarizeNote.ts) |
| **Elicitation** β server pauses to ask the user something | [`create-note`](src/server/tools/createNote.ts) (priority, if omitted) |
| **Roots** β client grants the server a specific local folder | [`index-roots`](src/server/tools/indexRoots.ts) |
| **Progress & logging notifications** | also `index-roots`, via [`sendNotification`/`sendLoggingMessage`](src/server/tools/indexRoots.ts) |
| **Pagination** | [`search-notes`](src/server/tools/searchNotes.ts), cursor-based |
| **Structured output** | every tool returns `structuredContent` alongside its text |
| **stdio vs. Streamable HTTP transports** | [`stdio.ts`](src/server/stdio.ts) / [`http.ts`](src/server/http.ts) |
| **Resumable transport** | [`eventStore.ts`](src/server/eventStore.ts), stateful HTTP mode |
| **Stateless transport** | `http.ts --stateless` |
| **OAuth 2.1 + PKCE authorization** | [`auth.ts`](src/server/auth.ts), `http.ts --oauth` |
## ποΈ Architecture
```mermaid
flowchart LR
subgraph client["interactiveClient.ts"]
cap["declares: sampling, roots, elicitation"]
end
subgraph transport["pick one"]
stdio["stdio.ts"]
http["http.ts (Streamable HTTP)"]
end
subgraph server["createServer.ts"]
t1["create-note<br/><i>elicitation</i>"]
t2["search-notes<br/><i>pagination</i>"]
t3["summarize-note<br/><i>sampling</i>"]
t4["index-roots<br/><i>roots + notifications</i>"]
store[("NotesStore<br/>(in-memory)")]
end
auth["auth.ts<br/>OAuth 2.1 + PKCE"]
evt["eventStore.ts<br/>resumability"]
client <-->|JSON-RPC over stdio or HTTP| transport
transport --> server
t1 & t2 & t3 & t4 --> store
http -. "--oauth protects" .-> auth
http -. "stateful mode uses" .-> evt
```
Sampling is the one genuinely bidirectional flow in the demo β the server calls back into
whichever process holds the model credentials β so it's worth seeing on its own:
```mermaid
sequenceDiagram
participant You
participant Client as interactiveClient.ts
participant Server as DevNotes server
You->>Client: summarize-note
Client->>Server: tools/call
Server->>Client: sampling/createMessage
alt ANTHROPIC_API_KEY set
Client->>Anthropic API: messages.create()
Anthropic API-->>Client: completion
else no key configured
Client->>Client: return a mock completion
end
Client-->>Server: CreateMessageResult
Server-->>Client: tool result (the summary)
Client-->>You: print it
```
The server never holds a model API key itself β that's the point of sampling. Only the
*client* in this diagram needs one, and only optionally (see [Quick start](#-quick-start)).
## π Quick start
```bash
npm install
cp .env.example .env # optional β see below
```
Everything runs with zero configuration. `.env` only matters if you want real Claude
completions for `summarize-note` instead of a mocked one β set `ANTHROPIC_API_KEY` there.
**Local (stdio) β what Claude Desktop and the MCP Inspector use:**
```bash
npm run client # spawns the server itself, opens an interactive prompt
# or, to poke at it directly:
npm run inspector
```
**Remote (Streamable HTTP):**
```bash
npm run start:http # stateful, resumable β default
npm run start:http:stateless # no session id, horizontally scalable
npm run start:http:oauth # requires a bearer token (see below)
npm run start:http:oauth-strict # + RFC 8707 resource-indicator enforcement
npm run client:http # talk to whichever one is running on :3000
```
**Try roots** by pointing the client at the bundled example notes:
```bash
npm run client -- --root ./examples
# then call index-roots from the prompt
```
**Try the full OAuth flow:** run `npm run start:http:oauth`, then open the MCP Inspector
(`npx @modelcontextprotocol/inspector`) and connect to `http://127.0.0.1:3000/mcp` β its
built-in "Connect" flow walks through dynamic client registration, the authorization
redirect, and the PKCE token exchange for you. `auth.ts` has a header-comment listing
exactly what it does and doesn't do β it's a real OAuth 2.1 authorization server, but an
intentionally minimal one (in-memory storage, no real login screen); the same file explains
what a production deployment needs to add back.
## π Project structure
```
src/
server/
createServer.ts factory: builds one McpServer + registers all four tools
store.ts the in-memory NotesStore (create/search/importFromRoot)
stdio.ts entrypoint: stdio transport
http.ts entrypoint: Streamable HTTP, stateful/stateless/oauth
auth.ts demo OAuth 2.1 + PKCE authorization server
eventStore.ts in-memory EventStore for resumable HTTP
tools/
createNote.ts elicitation
searchNotes.ts pagination
summarizeNote.ts sampling
indexRoots.ts roots + progress/logging notifications
client/
interactiveClient.ts demo client: implements the handlers a server can call back into
examples/ sample notes for the index-roots demo
assets/ the certificate this repo is a follow-up to
```
## π Notes on the security-relevant bits
- **Roots** are enforced entirely by the *client* β `store.importFromRoot` only ever reads
the `file://` URI it's handed, one directory level deep, capped at 200 files / 200 KB each.
The server never picks its own path.
- **`createMcpExpressApp`** enables DNS-rebinding protection automatically for `127.0.0.1` β
see [`http.ts`](src/server/http.ts).
- **`auth.ts`** is explicitly a demo authorization server (see the header comment in that
file for the exact list of what it skips). In a real deployment you'd almost always point
`verifyAccessToken` at an existing IdP rather than writing one.
- Every server-initiated request (`createMessage`, `listRoots`, `elicitInput`) passes
`relatedRequestId`, so responses route to the right session under concurrent HTTP load
instead of the first one that happens to be listening.
## πΊοΈ Learning path
Part of a series built while working through [Anthropic Academy](https://anthropic.skilljar.com/):
1. [`claude-api-starter`](https://github.com/Lazaro549/claude-api-starter) β Building with the Claude API
2. **`mcp-advanced-starter`** β MCP: Advanced Topics *(this repo)*
3. [`claude-code-starter`](https://github.com/lazaro549/claude-code-starter) β Claude Code in Action
## πΈ Donations
- π¦π· ARS (Argentina)
Alias: `lazaro.503.alaba.mp`
- π USD (local transfers within Argentina only)
Alias: `ahogada.duras.foca`
## License
MIT β see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues