Skip to main content
Glama

mcp-lab

CI E2E License: MIT Python 3.13 Checked with ruff

Learn the Model Context Protocol by reading a working server, then watching the protocol happen in your browser.

MCP's problem as a learning subject is that it's invisible: things work, and you never see why. This repository fixes that from two directions.

  1. A teaching server built on the official Python SDK (mcp 1.x, FastMCP) — every MCP primitive exactly once, heavily commented, meant to be read. The domain is a notes store, chosen because tools, resources, and prompts each have an obvious distinct role in it.

  2. A local dashboard that connects to real MCP servers as a real client, runs guided lessons against them, and shows you the actual JSON-RPC frames in transit — not a reconstruction.

NOTE

The comments are the product. This is a repository to read, not a library to depend on. If you only open one file, opensrc/notes_server/server.py.

Quick start

Needs uv and Python 3.13. Docker is optional — you only need it for the Postgres server.

git clone https://github.com/nikhilpatil79/mcp.git
cd mcp
uv sync --group dev
uv run pytest -q          # all green, no Docker or network needed

Then generate your local credentials (this writes .env with fresh random passwords on first run):

./scripts/setup-env.sh              # macOS / Linux
.\scripts\setup-env.ps1             # Windows

And start it:

docker compose up -d --wait    # 1. Postgres
uv run mcp-lab-ui              # 2. dashboard + UI  → http://127.0.0.1:8765

Or one command: ./scripts/start-ui.sh (.\scripts\start-ui.ps1 on Windows).

It's an ordinary local web app — no Claude account, no login, no browser extension. Any browser opens it. There is no separate backend: mcp-lab-ui serves the API and the page, and launches the MCP servers as child processes.

Full walkthrough, how to verify each layer, and what to do when something breaks: docs/RUNNING.md.

Related MCP server: MCP-PIF Server

The dashboard

/ — guided walkthroughs (start here)

Four short lessons. Each runs real calls against the servers on your machine and narrates what happened in plain English, one step at a time:

  1. What is a tool? — Claude discovers what a server offers, then uses one

  2. Tools vs resources vs prompts — the one distinction that matters: who decides to use it

  3. What happens when something goes wrong — why a failed tool isn't a crash

  4. Talking to a real database — a real SQL query, then a refused DELETE

Every step shows the plain-English explanation first, the result second, and the raw JSON-RPC messages last — behind a "show the actual messages" toggle. That ordering is deliberate: a protocol log only teaches you something once you already know what you're looking at.

Lesson 4 surfaces something genuinely worth knowing — servers don't agree on how to report a refusal. The notes server raises, so the protocol's isError flag gets set. postgres-mcp returns a normal result with an error message in the text. The UI labels each case distinctly rather than flattening them.

/pipeline — what CI/CD runs, and where Claude sits in it

The four CI stages, each with a run it now button that executes the local equivalent so you can watch it pass or fail — then the two Claude Code jobs, side by side, showing what each one does and what it cannot do.

The flags, triggers and permissions on that page are parsed out of .github/workflows/*.yml when you load it, so the page can't drift from the YAML. The commentary is hand-written, because parsing never produces an explanation.

/wire — the raw protocol dashboard

For once you know what you're looking at: every capability across all servers, an inspector to invoke anything, and a live JSON-RPC frame log.

The wire log is not a reconstruction. hub.py::_tap inserts a pair of memory streams between stdio_client and ClientSession and pumps messages across, logging each SessionMessage in transit — so you read the literal frame:

{ "method": "tools/call",
  "params": { "name": "search_notes", "arguments": { "query": "mcp" } },
  "jsonrpc": "2.0", "id": 7 }
WARNING

The dashboard is unauthenticated and can invokeevery tool on every connected server, including the Docker write tools. It binds to 127.0.0.1 deliberately — don't put it on a network interface. The .claude/settings.json allowlist governs Claude Code, not this UI. See SECURITY.md.

The connected servers

Server

What

Access

notes

The teaching server in src/

Read-write (local file)

postgres

Postgres MCP Pro → local postgres:17 container

Read-only, two enforced layers

github

Official GitHub server

Read-only, needs a PAT

docker

mcp-server-docker, 19 tools

Read + write ⚠️

The GitHub server is optional — leave the token unset and it reports as skipped rather than failing. Full setup, security model, and troubleshooting: docs/SERVERS.md.

docker compose up -d --wait                 # start Postgres
./scripts/setup-env.sh                      # or .\scripts\setup-env.ps1
uv run python scripts/drive_servers.py      # smoke-test all four
# then FULLY restart Claude Code, and check /mcp

drive_servers.py connects to each server as a real MCP client and exercises it, so you get the actual error instead of /mcp's connected/failed. It's also wrapped as a project skill (.claude/skills/run-mcp-servers/) — just ask Claude to run the MCP servers.

Permissions

.claude/settings.json pre-approves the read-only tools (all 9 Postgres, the 5 Docker list_*/fetch_*) and forces a prompt on the 14 Docker write tools and delete_note. The Docker server has no read-only mode, so this allowlist is the control — see docs/SERVERS.md.

Use it from Claude Code

.mcp.json is already wired up. Open this project in Claude Code, approve the servers when prompted, then:

> what MCP tools do you have available?
> save a note titled "MCP basics" saying MCP is JSON-RPC with agreed nouns, tag it mcp
> what have I written about mcp?

Check connection status any time with /mcp.

Poke at it in the Inspector

The fastest way to build intuition — a web UI that speaks the protocol, so you can list tools, call them, and read resources by hand:

uv run mcp dev src/notes_server/server.py

What's in the server

Tools (model-controlled — Claude decides to call these)

  • add_note — the minimal case; shows how docstring and type hints become the schema

  • search_notes — structured output via a pydantic model, plus readOnlyHint

  • delete_note — destructive annotations, and raising to signal failure

  • retag_notesContext injection for progress reporting and logging

Resources (application-controlled — the app loads these into context)

  • notes://stats — a static resource

  • notes://note/{note_id} — a resource template: one declaration, many URIs

Prompts (user-controlled — surface as slash commands)

  • summarise_tag — the simple single-string form

  • weekly_review — multi-message scaffolding

Subagents

Two, in .claude/agents/:

  • mcp-explainer — answers MCP concept questions from this repo's own code and docs. Read-only tools; grounded answers with file:line citations.

  • notes-librarian — operates the notes store through the MCP server. Its allowlist includes search/add/retag but deliberately excludes delete_note — destructive operations stay with the main agent where you can confirm them.

That exclusion is the point worth noticing: MCP tool annotations (destructiveHint) are an advisory hint to the client, while a subagent's tools: allowlist is actual enforcement. docs/CONCEPTS.md § Part 2 covers the distinction.

CI/CD

.github/workflows/ holds a working pipeline, written to be read:

Workflow

Runs when

Does

ci.yml

every push and PR

lint → protocol tests (Ubuntu + Windows) → live MCP servers against real Postgres → build

e2e.yml

nightly

boots the whole stack, runs every dashboard lesson, checks each behaved as advertised

release.yml

you push a v* tag

re-verifies the tag, publishes the wheel, pushes the Postgres image to GHCR

claude.yml

someone types @claude

Claude Code as a job, interactive mode

claude-review.yml

a PR opens

Claude Code as a job, automation mode, read-only

The integration job asserts the read-only Postgres boundary with a grep — a security control nobody tests is one you're only hoping about.

docs/CICD.md explains all of it: the CI/CD split, how to test an MCP server properly, the three kinds of credential in a pipeline, and what changes about Claude Code's permission model when there's no human to approve a tool call.

What to read, in order

File

What it teaches

src/notes_server/server.py

Start here. All three primitives, one numbered section each, with the reasoning inline

docs/CONCEPTS.md

The prose companion — architecture, message flow, primitive selection, and subagents

docs/CICD.md

The pipeline — CI/CD concepts, testing MCP servers, and running Claude Code as a job

tests/test_server.py

The same server seen from the client side — what the model actually receives

src/mcp_lab_ui/hub.py

How to be an MCP client, and how to tap the wire

src/notes_server/store.py

Plain storage, no MCP. Separate on purpose: an MCP server is a thin wrapper over capabilities you already have

Layout

src/notes_server/
  server.py     ← the annotated reference implementation
  store.py      ← plain JSON-backed storage, no MCP
  __main__.py   ← `python -m notes_server`
src/mcp_lab_ui/
  app.py        ← Starlette routes; thin adapters over Hub
  hub.py        ← one live MCP session per server, and the wire tap
  lessons.py    ← the guided lessons, and how a refusal is classified
  pipeline.py   ← parses .github/workflows/ so the page can't drift
  static/       ← learn.html, index.html, pipeline.html
tests/          ← protocol tests, store, hub, pipeline and route tests
docs/
  CONCEPTS.md   ← MCP concepts + subagents, explained
  SERVERS.md    ← the four connected servers, setup and security model
  RUNNING.md    ← starting and stopping it by hand
  CICD.md       ← the pipeline, and Claude Code running as a job
infra/postgres/init/
  01-schema.sql        ← seeded demo database
  02-readonly-role.sql ← the mcp_ro role the Postgres server logs in as
scripts/
  setup-env.{ps1,sh}   ← generate .env, sync secrets into the environment
  start-ui.{ps1,sh}    ← Postgres + dashboard in one command
  drive_servers.py     ← smoke-test every server in .mcp.json as a real client
  check_dashboard.py   ← run every lesson, assert each behaved as advertised
.github/
  dependabot.yml       ← grouped weekly updates for actions and dependencies
  workflows/           ← ci, nightly e2e, release, and two Claude Code jobs
  ISSUE_TEMPLATE/      ← bug report and unclear-explanation forms
.claude/
  settings.json                    ← permission allowlist (read vs write tools)
  agents/                          ← mcp-explainer, notes-librarian
  skills/run-mcp-servers/SKILL.md  ← how to run and debug the servers
CLAUDE.md            ← guidance for Claude Code working in this repo
docker-compose.yml   ← local Postgres
.mcp.json            ← registers all four servers with Claude Code
.env.example         ← credential template; .env itself is gitignored

Notes persist to notes.json at the project root; override with the NOTES_DB_PATH environment variable.

Contributing

See CONTRIBUTING.md. The review bar has one unusual item on it: because the comments are the product, a change that improves the code but degrades the explanation is a regression.

License

MIT.

Available Tools

4 tools
add_noteA

Save a new note.

Call this when the user wants something written down, remembered, or captured for later. Search first if a similar note might already exist.

ParametersJSON Schema
NameRequiredDescriptionDefault
bodyYesThe note's full text content.
tagsNoLowercase single-word tags, e.g. ['mcp', 'python'].
titleYesShort one-line title.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries the full burden. It correctly signals that this is a write operation and gives a useful behavioral warning that add_note may create duplicates if a similar note already exists. However, it does not disclose what happens on duplicate titles, whether tags are optional defaults, or the shape of the created-note response (though an output schema exists).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, each with a distinct job: state the action, give usage triggers, and instruct pre-call search. No filler or redundant restatement of the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a straightforward create operation with a complete schema and an output schema, this is nearly complete. The main gap is not explicitly naming the sibling search tool in the 'search first' instruction, but the existing sibling list makes the intended workflow recoverable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the input schema already documents title, body, and tags. The description adds no parameter-level detail, so it stays at the baseline for fully documented schemas.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with the precise verb-object pair 'Save a new note', identifying both the action and resource. It also marks the operation as creating rather than searching, deleting, or retagging, so it is clearly distinguishable from sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives concrete triggers ('when the user wants something written down, remembered, or captured for later') and instructs the agent to search first if a similar note may exist. It doesn't name the search tool explicitly or state a hard when-not-to-call condition, but the intended routing is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

delete_noteA
Destructive

Permanently delete a note by ID. Confirm with the user first.

ParametersJSON Schema
NameRequiredDescriptionDefault
note_idYesID of the note.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare destructiveHint=true, readOnlyHint=false, and idempotentHint=false, so the safety profile is known. The description adds useful context beyond that: the deletion is permanent, and the agent must obtain user confirmation before invoking. This goes beyond simply restating the annotation information.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short sentences with no fluff. The core action is front-loaded, and the critical user-confirmation requirement is stated immediately after. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple single-parameter deletion tool with a destructiveHint annotation and an output schema, the description covers the essential operational requirements: what is deleted, how it is identified, and the prerequisite confirmation step. Nothing necessary for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema already provides 100% coverage for the single parameter note_id, including a description 'ID of the note.' The description's phrase 'by ID' reinforces this but does not add new semantic detail beyond what the schema already gives. With full schema coverage, the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action ('Permanently delete') on a specific resource ('a note') with a clear access method ('by ID'). This clearly distinguishes it from the sibling tools add_note, search_notes, and retag_notes, all of which perform different operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The instruction to 'Confirm with the user first' provides a clear, explicit context for when this tool should be invoked: only after user confirmation. It does not explicitly list alternative tools or exclusion criteria, but the operation is distinct enough from the siblings that the primary usage context is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

retag_notesA

Rename a tag across every note that carries it.

ParametersJSON Schema
NameRequiredDescriptionDefault
new_tagYesTag to replace it with.
old_tagYesTag to replace.

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description carries full behavioral disclosure. It states the bulk/global nature of the operation, which is the key behavioral trait. However, it does not mention what happens if new_tag already exists (e.g., merging), whether the operation is reversible, or that it mutates notes across the entire workspace. The schema labels parameters but not side effects.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence that front-loads the action and scope with no filler words. Every word earns its place: 'rename', 'tag', 'across every note', and 'carries it' all contribute to the tool's purpose and behavioral scope.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a two-parameter tool with full schema coverage and an output schema, the description is largely complete for correct selection and invocation. It could clarify behavior when new_tag already exists, but this is a minor edge case. The core usage context and parameter semantics are both adequately covered.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, with both old_tag and new_tag described in the input schema. The description adds no parameter-level details beyond the schema. It does add useful context about global scope, but this is unnecessary for parameter interpretation. A baseline 3 is appropriate since the schema already documents the parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action ('rename') applied to a specific resource (a tag) with clear global scope ('across every note that carries it'). This cleanly distinguishes the tool from siblings like add_note, search_notes, and delete_note, which operate on notes rather than tags.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The global scope makes it clear this is the tool for renaming a tag across all notes rather than within a single note. There are no explicit when-not-to-use statements or alternatives, but the sibling set contains no competing tag-rename tool, so the invocation context is unambiguous. The description could have been more explicit about per-note tag edits being out of scope, but it is clear enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_notesA
Read-only

Find notes matching a text query and/or a tag.

An empty query with no tag returns every note. Use this before adding a note to avoid creating a near-duplicate.

ParametersJSON Schema
NameRequiredDescriptionDefault
tagNoOptional tag filter.
queryNoText to match in title or body.

Output Schema

ParametersJSON Schema
NameRequiredDescription
countYes
notesYes
queryYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark this readOnlyHint=true. The description adds the important boundary behavior that an empty query with no tag returns every note, which an agent might otherwise treat as an invalid or no-op call. It does not conflict with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two compact sentences: the first states the function, the second states the boundary condition and gives a concrete use case. No filler or repetition of the schema.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only 2-optional-param search tool with an output schema, the description covers what it searches, the empty-result boundary, and the main workflow context. Nothing essential is missing for an agent to select and invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds the interaction rule for query and tag ('and/or') and specifies the empty-query-with-no-tag behavior, which the schema does not. It could be more precise about how the two filters combine, but it adds genuine value beyond the schema fields.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific action ('Find notes') and the matching criteria (text query and/or tag), making the tool's purpose unambiguous. It also distinguishes itself from the mutation siblings by its read-only search nature and the explicit duplicate-avoidance use case.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly tells the agent to use this tool before adding a note to avoid near-duplicates, which is concrete usage guidance. It doesn't list exclusion cases or alternative search tools, but there are no other search siblings, so the context is sufficient.

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. Dates show when Glama detected each change.

  1. 4 tool updatesv0.1.0
    • First observedadd_note
    • First observeddelete_note
    • First observedretag_notes
    • First observedsearch_notes

TDQS

A4.2/5.0

Scored across 4 tools

Disambiguation5/5

Each tool targets a distinct operation: creating notes, searching notes, deleting notes, and renaming tags. There is no overlap in purpose or ambiguity about which tool to call.

Naming Consistency4/5

All tool names follow a clear verb_noun pattern: add_note, search_notes, delete_note, retag_notes. The pluralization is slightly inconsistent (notes vs note), but the pattern is predictable and easy to follow.

Tool Count5/5

Four tools is a well-scoped set for a simple note management server. Each tool covers a distinct need without redundancy or bloat.

Completeness3/5

The server supports add, search, delete, and tag renaming, but lacks a way to edit a note's content or fetch a single note directly by ID. These are notable gaps in the core note lifecycle, though agents can work around some of them via search and deletion.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nikhilpatil79/mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server