Tuesday, 11 August 2026

MCP Server

 

What is an MCP server and why is it different from wiring a tool directly?

When wiring a tool directly into an application, you are responsible for defining the tool’s schema and its functionality. Both live in that application’s code. If three different applications need access to the same external service, each one maintains its own integration. Model Context Protocol, or MCP, separates tool definitions from individual applications and turns them into a process called a server.

An MCP server is a process that exposes tools, resources, and prompts that MCP clients can use. 

The practical difference: a tool is the model reaching out to get something when it decides it needs it. A resource is the client pulling known data in before the model even starts. A prompt is packaging expert-level instructions so every developer on your team gets the same quality output without having to know how to write that instruction themselves.

Three things an MCP server exposes — you already know tools, here are the other two.

Resources — read-only data fetched directly into context

A resource is data the server exposes at an address. The client fetches it and places it into context directly — no tool call, no model decision involved. Think of it as a GET request to a known endpoint.

Two forms:

  • Direct resource — fixed address, no parameters. The data is always at that address.
  • Templated resource — address contains a parameter, resolved at fetch time.

When to use it over a tool: When you know upfront what data you need in context before the turn starts. A tool call is dynamic — the model decides when and whether to call it. A resource is deterministic — the client fetches it directly. Cheaper and more predictable when the data is known.

Practical examples:

# Direct resource — fixed address
mcp://java-tools/conventions/spring-boot-standards
→ Returns your team's Spring Boot coding standards
→ Client fetches this at session start, always in context

# Templated resource — parameterized address  
mcp://java-tools/schema/{tableName}
→ mcp://java-tools/schema/orders
→ Returns the DDL for the orders table
→ Client fetches by passing the table name as parameter

Other real-world examples:

  • mcp://jira/project/OMS/open-tickets — current sprint tickets loaded into context
  • mcp://confluence/runbooks/{serviceName} — runbook for a specific service
  • mcp://github/repo/accenture-oms/pull-requests/open — open PRs fetched directly

Prompts — pre-written instruction templates invoked by name

A prompt is a vetted instruction template the server exposes so any client can invoke it by name instead of writing their own version. The server maintains it in one place, every connected client gets the same quality.

When a prompt is worth it over just asking Claude: When specific wording materially changes the output quality. For routine questions the user's own words are fine. For specialized tasks — a security review, a database migration audit, an API contract check — a carefully constructed prompt produces consistently better results than whatever each developer would type on the fly.

Practical examples for your Spring Boot context:

# Invoked by name in the client
/prompt: review-db-migration

# What the server actually sends to the model
"Review this Flyway migration file against these criteria:
1. Does it have a matching rollback script in db/rollbacks/?
2. Are all foreign key columns indexed?
3. Does it use CREATE OR REPLACE anywhere (not allowed)?
4. Are all VARCHAR columns explicitly sized?
5. Is the version number in the correct format V{date}_{seq}?
Return findings as: PASS / WARN / FAIL with specific line references."
# Another example
/prompt: api-contract-review

# Server sends
"Review this REST controller against our API standards:
1. Are all endpoints using method-specific annotations (@GetMapping etc)?
2. Are all response bodies Java records, never raw entities?
3. Is business logic absent from the controller?
4. Are all error responses using ProblemDetail (RFC 9457)?
5. Does every endpoint have @Operation and @ApiResponse annotations?
Return a checklist with PASS/FAIL per item."

How all three fit together

MechanismWhat it isWhen to reach for it
ToolAction the model calls dynamicallyWhen the model needs to decide whether and when to fetch or act
ResourceRead-only data fetched directly into contextWhen you know upfront what data you need, before the turn starts
PromptPre-written instruction template invoked by nameWhen specific wording produces materially better results than freeform input


Three transports, each for a different deployment topology.

The MCP transport (stdio, HTTP, SSE) just carries JSON-RPC envelopes


stdio — server runs as a child process on the same machine

The client launches the server as a subprocess and communicates via stdin/stdout. One line in, one line out — JSON-RPC over pipes. This is exactly what your ClaudeMCPServer.java uses. No network, no port, no authentication needed. The process lives and dies with the client session.

Use it when: the server runs locally alongside Claude Code on the same machine.


HTTP — server runs as a standalone process, local or remote

The client sends HTTP POST requests to a fixed URL. The server is a normal HTTP service — could be on localhost, could be on a remote host. Stateless request/response, same as any REST API. Each request is independent.

Use it when: the server needs to be shared across multiple clients, runs on a remote machine, or needs to persist beyond a single session.


SSE (Server-Sent Events) — HTTP with a persistent stream back

An extension of HTTP transport. The client POSTs requests normally, but the server holds the connection open and streams responses back as events rather than closing after each response. Useful when the server needs to push notifications or stream partial results.

Use it when: the server needs to send unsolicited updates to the client, or results arrive incrementally.


The practical decision

Server on same machine, local dev    →  stdio
Server shared across team / remote   →  HTTP
Server needs to push events          →  SSE

Your ClaudeMCPServer.java is stdio — that's why System.out is the protocol wire and System.err is the only safe place to log. Switching to HTTP would mean replacing stdin/stdout with an HTTP listener, but the JSON-RPC message structure stays identical.



Plugins

 Plugins — one-step installation of your entire setup

A plugin bundles your skills, hooks, subagents, and MCP servers into a single versioned installable unit. Instead of a teammate manually copying your .claude/ directory, configuring hooks, and wiring MCP servers — they run one install command and get the identical setup.

Structure of what a plugin contains:

plugin/
  skills/          ← your .md skill files
  hooks/           ← PreToolUse, PostToolUse scripts
  agents/          ← custom subagent definitions
  settings.json    ← MCP servers, permissions
  manifest.json    ← describes the bundle

Marketplace — where plugins are distributed

  • Anthropic's official marketplace is available automatically in Claude Code
  • Add third-party marketplaces (hosted on GitHub) with /plugin marketplace add <owner/repo>
  • Enterprise admins can deploy plugins org-wide via managed settings — sits above user and project settings, cannot be overridden

The decision table — which layer to reach for

LayerReach for it when
SkillA procedure should stay out of context until the task calls for it
Custom commandThe procedure has a clear name and you want explicit invocation
PluginA working setup on your machine needs to be shared, versioned, and kept consistent across a team

The one risk worth remembering

A deny rule or hook you rely on locally is not included in a plugin unless explicitly listed in the bundle. If your guardrails aren't part of the plugin manifest, your teammates install the skills without the safety net you built around them. Always audit what the plugin actually bundles before distributing it.

Installation success and Execution success are two different things.

What went wrong

A developer built a deployment skill, tested it locally, packaged it as a plugin, pushed it to the internal marketplace. Every teammate's install succeeded. Every teammate's execution failed.

Two root causes in the same SKILL.md:

bash
# Failure 1 — absolute path to author's home directory
/Users/joseph/projects/deploy-utils/validate.sh

# Failure 2 — environment variable set only in author's shell profile
$DEPLOY_TOKEN

The first failure is visible — a reviewer reading the file can catch it immediately. The second is invisible — nothing in the package announces the dependency, the skill runs fine until the exact step that needs $DEPLOY_TOKEN, and only then fails silently. That's why it cost three people two hours to debug.


The fix — four rules

1. No absolute paths, ever.
Use $CLAUDE_PROJECT_DIR for scripts stored in the project, ${CLAUDE_PLUGIN_ROOT} for scripts bundled inside the plugin itself.

bash
# Wrong
/Users/joseph/projects/deploy-utils/validate.sh

# Right
${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh

2. Bundle everything the plugin needs.
Scripts, config files, assets — either inside the plugin or in a shared project location every teammate gets on install. Nothing that only exists on the author's machine.

3. Document and validate every environment variable at install time.
Don't let a missing variable surface mid-run. Surface it immediately when the plugin installs, not two hours into a debugging session.

4. Test on a clean machine before distributing.
Your own machine hides problems. A clean machine reveals them instantly.


The underlying principle

The author's machine is not the team's machine. A plugin that only works on the author's setup isn't a plugin — it's a local script with extra packaging steps. Portability has to be deliberately designed in, because nothing in the installation process will warn you it's missing.