| name | mcp-extend |
| description | Use when a custom MCP server doesn't have a tool needed for an operation in your project — decides whether to add a tool-only wrapper (if backend endpoint exists) or to add a new backend endpoint + tool, then walks through the implementation and verification steps. |
Extending a Custom MCP Server
Your project may have one or more custom MCP servers that wrap backend HTTP endpoints so Claude (and other agents) can perform domain operations consistently. When a needed tool is missing — decide between tool-only (endpoint exists) and endpoint + tool (endpoint missing). Never bypass MCP with raw SQL if the project's invariants depend on event publishing / cache sync.
Step 1 — Decide: tool-only or endpoint + tool
Search for the operation in existing admin endpoints:
grep -rn "MapPost\|MapPatch\|MapDelete\|MapPut" \
backend/ --include="*.cs" | grep -i "<operation-keyword>"
(Adapt to your stack — app.get / app.post for Express, router.handle_func for Go, @Router for FastAPI, etc.)
- Look in your project's
Features/<Entity>/UseCases/ (or equivalent) for matching handlers.
- If endpoint exists with a compatible shape → go to Step 2.
- If not → go to Step 3.
Do not add a "mega-tool" that bundles multiple operations. One MCP tool = one HTTP call. Batching is done on the agent side via composition.
Step 2 — Add a tool (endpoint exists)
Typical file layout for a TypeScript MCP server:
mcp/<server>/src/tools/
<domain1>.ts
<domain2>.ts
...
Tool naming: <service-prefix>_<entity>_<action>. Pick prefixes once and stick to them (e.g., edu_, auth_, files_). Follow URL shape: PATCH /api/modules/{id} → <prefix>_module_update; POST /api/modules/{id}/issues/{issueId} → <prefix>_module_item_attach_issue.
Tool shape (TypeScript SDK):
import { z } from 'zod';
import { defineTool } from '../tool.ts';
export const attachIssueToModule = defineTool({
name: 'edu_module_item_attach_issue',
description: 'Attach an existing issue as an item inside a module. Creates module_items row with auto-computed sort_key at end. Publishes issue.bound event.',
inputSchema: z.object({
moduleId: z.string().uuid(),
issueId: z.string().uuid(),
}),
handler: async ({ moduleId, issueId }, { client }) => {
const { data } = await client.post(`/api/modules/${moduleId}/issues/${issueId}`);
return { moduleItemId: data };
},
});
Rules:
- Input: only IDs and primitives. Never large DTOs.
- Output: structured JSON. Include ID of created / modified entity.
- Description: what it does and what events fire (Claude needs this for consistency reasoning).
- Never catch errors — let them bubble to the framework-level mapper that converts HTTP errors →
McpError.
After adding:
- Register in the right tool collection file.
- Build clean (
npm run build for TS, strict mode).
- Add a contract test that hits the locally-running platform.
- Reload MCP in Claude (
/mcp → reload).
- Smoke-test once: invoke the new tool with a known-good ID.
Step 3 — Add endpoint + tool (endpoint missing)
3.1. Backend endpoint
- Pick the right service / module.
- Create the handler file containing Command/Query record + Validator + Endpoint + Handler (vertical slice).
- Follow existing file conventions (use a similar handler as a template).
- Permissions: use the project's permission attributes (
.RequirePermissions(...), @require_role, decorators, etc.). Admin-only operations get the admin permission.
- Publish integration events if the mutation changes content accessibility — see root
CLAUDE.md messaging section. Missing events = downstream cache drift.
- If touching tables from a data fix — use the project's data-migration pattern (e.g.,
DataMigrationRunner with embedded SQL). Never raw SQL via MCP.
- Add Contracts DTO if the shape is used by another service.
- Integration test covering the happy path + one forbidden case.
3.2. Build and verify backend
dotnet build backend/backend.slnx
dotnet test backend/<Service>/tests/<Service>.IntegrationTests
3.3. Add the MCP tool (go to Step 2)
3.4. Deploy backend (if prod)
Local: bring up your dev stack so the new endpoint is reachable.
Prod: commit → integration branch → main → CI deploys. Never call a newly-added endpoint on prod before it's deployed.
Step 4 — Consistency checklist before using the new tool
Step 5 — Commit conventions
One commit per logical step:
feat(<service>): add GET /api/<entity>/admin-all endpoint
feat(mcp): add <prefix>_<entity>_list_all tool
Commit message explains why (what was the missing capability, not just what changed).
Red flags (stop and think)
| Symptom | Likely cause |
|---|
| Tool doesn't show up in Claude after reload | Forgot to register in tool collection / build failed silently |
| 401 Unauthorized on call | Client secret rotated / expired token / wrong client_id |
| 403 Forbidden | Required role missing from token — check token mapping |
| 404 on attach / detach | Confusion between reference id and join-row id — pass the entity id, not the join-table id |
| Operation succeeds but UI doesn't reflect change | Event not published / outbox not processed / cache out of sync — check service logs |
| Tool call corrupts ordering | Fractional indexing bug — verify reference items exist in the same parent |
Where things live (adapt to your project)
| Concern | Typical location |
|---|
| MCP server code | mcp/<server>/ |
| Tool implementations | mcp/<server>/src/tools/*.ts |
| Auth / token caching | mcp/<server>/src/auth.ts |
| HTTP client + retry | mcp/<server>/src/client.ts |
| Claude MCP config | ~/.claude/mcp.json (user-level) or project .mcp.json |
| Admin client secret | .env on dev / secrets manager on prod |
| Backend admin endpoints | backend/<Service>/src/<Service>.Core/Features/ |
| Permissions constants | backend/Shared/Authentication/PlatformPermissions.cs |
| Role → permissions | backend/Shared/Authentication/RolePermissions.cs |