Debugging MCP Servers
Debugging MCP Servers
Section titled “Debugging MCP Servers”Practical, verified guide to debugging Model Context Protocol servers, applied to the @satware/onlyoffice-mcp project. All commands and claims below were executed on 2026-07-01 against the running Docker deployment.
Sources
Section titled “Sources”- https://modelcontextprotocol.io/docs/tools/debugging - debugging overview
- https://modelcontextprotocol.io/docs/tools/inspector - MCP Inspector guide
- Inspector source:
~/external/mcp-inspector(cloned from https://github.com/modelcontextprotocol/inspector, tag 0.22.0)
1. Tool ladder (use in this order)
Section titled “1. Tool ladder (use in this order)”| # | Tool | When | Captures |
|---|---|---|---|
| 1 | MCP Inspector | First stop. Transport-agnostic interactive UI. | initialize exchange, capability negotiation, tool calls, resource reads, prompt renders, notification stream |
| 2 | Server logging | Always. stderr (stdio) or notifications/message (HTTP) |
Init steps, resource access, tool exec, errors, timings |
| 3 | Client dev tools | After Inspector passes, test real client. | Connection events, message payloads, network timing |
2. MCP Inspector
Section titled “2. MCP Inspector”2.1 Install / run (verified)
Section titled “2.1 Install / run (verified)”The Inspector runs directly through npx; no install required. Verified version 0.22.0:
npx @modelcontextprotocol/inspector --help# Options:# -e <env> environment variables in KEY=VALUE format# --config <path> config file path# --server <n> server name from config file# --cli enable CLI mode# --transport <type> transport type (stdio, sse, http)# --server-url <url> server URL for SSE/HTTP transport# --header <headers...> HTTP headers as "HeaderName: Value" pairsDefault mode opens a browser UI on localhost:6277. Use --cli for non-interactive runs.
2.2 Inspecting local servers (verified patterns)
Section titled “2.2 Inspecting local servers (verified patterns)”# TypeScript server built to dist/index.js (stdio transport)npx @modelcontextprotocol/inspector node /home/mw/internal/onlyoffice/dist/index.js
# Streamable HTTP server - launch Inspector UI, pick "Streamable HTTP", enter URLnpx @modelcontextprotocol/inspector# URL field: http://localhost:3847/mcp OR https://onlyoffice.localhost/mcp
# CLI mode against HTTP transport (non-interactive)npx @modelcontextprotocol/inspector --cli --transport http --server-url https://onlyoffice.localhost/mcp2.3 Inspector UI panes
Section titled “2.3 Inspector UI panes”| Pane | Purpose |
|---|---|
| Connection | Choose transport (stdio / Streamable HTTP / SSE), set args + env |
| Resources | List, MIME types, content preview, subscription test |
| Prompts | List templates, args, preview generated messages |
| Tools | Schemas, invoke with custom inputs, see results |
| Notifications | Live log + notification stream from server |
3. Logging rules (verified against SDK)
Section titled “3. Logging rules (verified against SDK)”3.1 Transport-specific behaviour
Section titled “3.1 Transport-specific behaviour”| Transport | stderr captured by client? | Logging mechanism |
|---|---|---|
| stdio | YES (host captures stderr automatically) | process.stderr.write() |
| Streamable HTTP | NO (per docs: “stderr is not captured by the client”) | server.sendLoggingMessage({level, data}) |
WARNING (from docs): stdio servers MUST NOT log to stdout - it corrupts the JSON-RPC protocol.
3.2 sendLoggingMessage API (verified in SDK)
Section titled “3.2 sendLoggingMessage API (verified in SDK)”Confirmed exists in @modelcontextprotocol/sdk v1.28.0:
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.d.ts:198sendLoggingMessage( params: LoggingMessageNotification['params'], sessionId?: string): Promise<void>;Usage in a tool handler:
await server.sendLoggingMessage({ level: 'info', data: 'Server started successfully',});Eight RFC 5424 severity levels (debug → emergency). Clients adjust the minimum level at runtime via logging/setLevel.
4. Common failure classes
Section titled “4. Common failure classes”| Symptom | Likely cause | Fix |
|---|---|---|
| Server won’t start | Wrong command path, missing files, perms |
Use absolute paths in config |
| Missing env vars | stdio inherits limited env subset | Pass env in client config |
-32602 Invalid params |
Server sent sampling/elicitation to client that didn’t declare that capability | Inspect initialize exchange; both sides must declare capabilities |
| Connection drops | Session mismatch on HTTP | Verify Mcp-Session-Id header round-trips |
| Config errors | Invalid JSON, missing fields | Validate config file syntax |
5. Debugging workflow
Section titled “5. Debugging workflow”- Dev loop: Inspector for basic connectivity → implement → add logging points
- Integration: switch to target client, monitor logs, check error handling
- Iteration: config changes → restart client; server code changes → fully quit + reopen client (closing window is not enough for Claude Desktop); quick iteration → stay in Inspector
6. Best practices
Section titled “6. Best practices”- Structured logging: consistent format, timestamps, request IDs
- Log stack traces + error context, not just messages
- Track operation timing + message sizes
- Sanitize secrets/PII from logs (OnlyOffice has
jwtSecret, credentials - mask them)
Applied to ~/internal/onlyoffice
Section titled “Applied to ~/internal/onlyoffice”7. Server profile (verified)
Section titled “7. Server profile (verified)”| Aspect | Value | Verification |
|---|---|---|
| Transport (primary) | Streamable HTTP | src/server.ts:3 |
| Transport (secondary) | stdio | src/index.ts:3, package.json bin onlyoffice-mcp |
| HTTP endpoint | http://localhost:3847/mcp (dev) / https://onlyoffice.localhost/mcp (Caddy proxy) |
docker ps shows onlyoffice_mcp_server on 0.0.0.0:3847 |
| Tools registered | 20 total | tools/list via stdio returned 20 tool names |
| Resources registered | stdio entry only | grep registerResource matches only in src/index.ts, not src/server.ts |
| Session mgmt | Mcp-Session-Id header, Map<sid, StreamableHTTPServerTransport> |
src/server.ts:114 |
| Health | GET /health returns {"status":"healthy","version":"1.1.0",...} |
curl https://onlyoffice.localhost/health → 200 |
| SDK version | @modelcontextprotocol/sdk@^1.28.0 |
package.json:72 |
8. Verified tool inventory (via stdio tools/list)
Section titled “8. Verified tool inventory (via stdio tools/list)”onlyoffice_generate_documentonlyoffice_batch_generateonlyoffice_merge_documentsonlyoffice_split_documentonlyoffice_list_templatesonlyoffice_modify_documentonlyoffice_validate_dataonlyoffice_get_metadataonlyoffice_set_metadataonlyoffice_extract_contentonlyoffice_extract_textonlyoffice_extract_tablesonlyoffice_extract_imagesonlyoffice_add_watermarkonlyoffice_storage_readonlyoffice_storage_writeonlyoffice_storage_listonlyoffice_storage_deleteonlyoffice_storage_existsonlyoffice_list_instances(20 tools. Credential tools are conditional on CREDENTIAL_MASTER_PASSWORD; the dev container has it set, so they would push the count higher if registered - but the stdio test above did NOT pass the env var, so credential tools were absent.)
9. Verified resource inventory (via stdio resources/list)
Section titled “9. Verified resource inventory (via stdio resources/list)”onlyoffice://templatesonlyoffice://templates/stellenbeschreibung/schemaonlyoffice://templates/dienstliches_schreiben/schema... (one per registered template)onlyoffice://documents/{id} (ResourceTemplate, listed dynamically)Resources are registered only in src/index.ts:95-123 (stdio entry). The HTTP entry (src/server.ts) does not call registerResource. To inspect Resources via the Inspector, you MUST use the stdio transport.
10. BUG FOUND & FIXED: Streamable HTTP endpoint (#84, FIXED 2026-07-01)
Section titled “10. BUG FOUND & FIXED: Streamable HTTP endpoint (#84, FIXED 2026-07-01)”Status: FIXED. Fix merged in commit
c6e912e(fix(mcp): fresh McpServer per session — /mcp no longer 500 after first (#84)). Verified working 2026-07-01 13:36 UTC+2. See §10.6 for verification results.
10.1 Symptom (verified)
Section titled “10.1 Symptom (verified)”Every initialize request to https://onlyoffice.localhost/mcp (and http://localhost:3847/mcp) returns HTTP 500 {"error":"Internal error"}.
$ curl -i http://localhost:3847/mcp -X POST \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'HTTP/1.1 500 Internal Server Error{"error":"Internal error"}10.2 Root cause (verified in SDK source)
Section titled “10.2 Root cause (verified in SDK source)”src/server.ts creates ONE mcpServer instance and calls mcpServer.connect(t) for each new session:
// src/server.ts:87const mcpServer = new McpServer({ name: 'onlyoffice-mcp', version });
// src/server.ts:122-123 (inside handleStreamableTransport, per new session)t = new StreamableHTTPServerTransport({ sessionIdGenerator: ... });await mcpServer.connect(t); // <-- throws on 2nd callThe SDK’s Protocol.connect() throws if _transport is already set:
// node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js:215-218async connect(transport) { if (this._transport) { throw new Error('Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'); } ...}10.3 Evidence (Docker logs)
Section titled “10.3 Evidence (Docker logs)”$ docker logs --tail 10 onlyoffice_mcp_serverError: Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.Error: Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.Error: Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.10.4 Impact
Section titled “10.4 Impact”- The first session after container start succeeds; every subsequent session fails with 500.
- Inspector, TypingMind, OpenClaw, and any other HTTP client cannot connect once the first session is held.
- This explains why the production endpoint at
https://onlyoffice.localhost/mcpis unusable for debugging via Inspector.
10.5 Fix (implemented in commit c6e912e)
Section titled “10.5 Fix (implemented in commit c6e912e)”Per the SDK error message itself: “use a separate Protocol instance per connection”. The devs extracted the McpServer construction + all tool registrations into a factory (src/mcp-server-factory.ts). src/server.ts calls buildServer() per new session; the transports Map tracks { server, transport } pairs so DELETE /mcp closes both (server.ts:120-122).
Actual implementation (src/mcp-server-factory.ts):
export function createMcpServerFactory(deps, serverInfo): () => McpServer { return (): McpServer => { const server = new McpServer({ name: serverInfo.name, version: serverInfo.version }); registerBuilderTools(server, { ... }); registerContentExtractionTools(server); registerWatermarkTools(server); registerStorageTools(server); registerInstanceTools(server, deps.registry); if (deps.credentialManager) registerCredentialTools(server, deps.credentialManager); return server; };}src/server.ts usage:
// server.ts:83 - transports Map now tracks pairsconst transports = new Map<string, { server: McpServer; transport: StreamableHTTPServerTransport }>();
// server.ts:92 - fresh server per new sessionconst server = buildServer();
// server.ts:120-122 - DELETE closes bothawait entry.transport.close();await entry.server.close();transports.delete(sId);11. Recommended debug setup for OnlyOffice
Section titled “11. Recommended debug setup for OnlyOffice”11.1 Verification (2026-07-01 13:36 UTC+2, post-fix)
Section titled “11.1 Verification (2026-07-01 13:36 UTC+2, post-fix)”Container rebuilt and restarted with the fix. All issue #84 acceptance criteria pass:
| Test | Method | Result |
|---|---|---|
| Container runs new code | docker exec ... ls /app/dist/mcp-server-factory.js |
OK - exists; grep -c buildServer /app/dist/server.js = 4 |
/health 200 |
curl https://onlyoffice.localhost/health |
OK - v1.1.0 |
5 sequential initialize |
curl loop to /mcp |
OK - all 200, 5 unique sids, no errors in logs |
DELETE /mcp valid sid |
curl -X DELETE -H "mcp-session-id: <sid>" |
OK - 200 {"message":"Terminated"} |
DELETE /mcp deleted sid |
second DELETE same sid | OK - 404 {"error":"Not found"} |
DELETE /mcp unknown sid |
DELETE random UUID | OK - 404 {"error":"Not found"} |
| New session after DELETE | POST initialize after DELETE | OK - 200, new sid |
tools/list via HTTP |
curl POST with session | OK - 25 tools |
| Inspector CLI session 1 | npx @modelcontextprotocol/inspector --cli http://localhost:3847/mcp --method tools/list |
OK - 25 tools |
| Inspector CLI session 2 | same command, second run | OK - 25 tools |
| Inspector CLI session 3 | same command, third run | OK - 25 tools |
| Tool invocation via Inspector | --method tools/call --tool-name onlyoffice_list_templates |
OK - template list JSON |
| Inspector via HTTPS proxy | --cli https://onlyoffice.localhost/mcp |
OK with NODE_TLS_REJECT_UNAUTHORIZED=0 (self-signed cert - Inspector has no --insecure flag; not a server bug) |
resources/list via HTTP |
--method resources/list |
-32601 Method not found - by design, Resources are stdio-only (§9) |
| Docker logs after all tests | docker logs --tail 20 |
Only New:/Closed: lines, zero Error: lines |
Conclusion: Fix is correct and complete. The /mcp endpoint now supports unlimited concurrent/sequential MCP client sessions.
11.2 Inspector against stdio (works today, covers Resources tab)
Section titled “11.2 Inspector against stdio (works today, covers Resources tab)”cd ~/internal/onlyoffice && npm run buildnpx @modelcontextprotocol/inspector node /home/mw/internal/onlyoffice/dist/index.jsUse this to test all 20 tools and all 3 resource templates. The Resources tab will populate.
11.3 Inspector against Streamable HTTP (now working, post-fix #84)
Section titled “11.3 Inspector against Streamable HTTP (now working, post-fix #84)”Once the §10 bug is fixed (DONE - commit c6e912e):
npx @modelcontextprotocol/inspector11.4 HTTP-level debugging without Inspector
Section titled “11.4 HTTP-level debugging without Inspector”# Initialize (capture Mcp-Session-Id from response headers)curl -i -X POST http://localhost:3847/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
# Browser DevTools Network panel against https://onlyoffice.localhost/mcp:# - inspect SSE stream chunks# - inspect Mcp-Session-Id header round-trip# - inspect request timing11.5 Server-side logging gap
Section titled “11.5 Server-side logging gap”src/server.ts uses process.stderr.write() exclusively. Per the docs and SDK, stderr is NOT captured by the client on Streamable HTTP - so Inspector’s Notifications pane will NOT show those messages even after the §10 fix. To make server logs visible in Inspector, add sendLoggingMessage calls in tool handlers:
// In builder-tools.ts etc:await server.sendLoggingMessage({ level: 'info', data: `generateDocument: template=${id}` });await server.sendLoggingMessage({ level: 'error', data: `DocBuilder failed: ${err.message}` });This is the highest-value change for debugging the OnlyOffice MCP server through Inspector over HTTP.
12. Inspector source reference
Section titled “12. Inspector source reference”~/external/mcp-inspector (v0.22.0) - useful for:
server/+client/- how Inspector sendsinitializeand negotiates capabilities (mirror in integration tests)- How it renders tool schemas (catch schema-extraction regressions for the admin UI tree editor)
sample-config.json- stdio server arg format reference
13. Verification log (2026-07-01)
Section titled “13. Verification log (2026-07-01)”| Claim | Method | Result |
|---|---|---|
| Inspector v0.22.0 runs via npx | npx @modelcontextprotocol/inspector --help |
OK - prints help with --transport, --server-url, --cli |
| OnlyOffice Docker container running | docker ps |
OK - onlyoffice_mcp_server on 0.0.0.0:3847 |
/health returns 200 |
curl https://onlyoffice.localhost/health |
OK - {"status":"healthy","version":"1.1.0"} |
/mcp returns 500 |
curl http://localhost:3847/mcp initialize |
BUG - 500 Internal error |
| stdio entry works | pipe initialize JSON to node dist/index.js |
OK - returns protocolVersion + capabilities |
| Resources are stdio-only | grep registerResource in src/server.ts |
CONFIRMED - 0 matches in server.ts, all in index.ts |
sendLoggingMessage exists |
grep in SDK .d.ts |
OK - mcp.d.ts:198 |
| 20 tools registered | stdio tools/list |
OK - 20 names returned |
| “Already connected” error source | grep SDK source | OK - protocol.js:217 |
| Docker logs show the error | docker logs onlyoffice_mcp_server |
OK - 3+ occurrences |
14. Next steps
Section titled “14. Next steps”Fix §10 bug- DONE (commitc6e912e, issue #84 verified closed)- Add
sendLoggingMessagecalls to tool handlers so Inspector Notifications pane shows server logs over HTTP (still pending) - Write an Inspector-based smoke test in
~/internal/onlyoffice/tests/integration/exercisinginitialize->tools/list->generateDocument - Document the Inspector debug recipe in
~/internal/onlyoffice/docs/
Appendix A: MCP Resources Explainer
Section titled “Appendix A: MCP Resources Explainer”Source: https://modelcontextprotocol.io/docs/concepts/resources and
~/external/modelcontextprotocol/docs/specification/2025-11-25/server/resources.mdx. Verified against the OnlyOffice stdio entry on 2026-07-01.
A.1 What resources are
Section titled “A.1 What resources are”Resources are server-exposed, read-only context data identified by URIs that the host application (not the model) curates and injects into the conversation. The host decides how to surface them - a tree/list UI, a search filter, or auto-inclusion heuristics. The protocol mandates no specific UX.
Key contrast: Resources vs Tools
Section titled “Key contrast: Resources vs Tools”| Resources | Tools | |
|---|---|---|
| Who initiates | Application / user picks what to include | Model decides to call |
| Direction | Read-only context the host injects | Action the model invokes |
| Returns | Text or base64 blob at a URI | Arbitrary result (text, artifacts, side effects) |
| Methods | resources/list, resources/read, resources/subscribe |
tools/list, tools/call |
| Use case | “Show me what’s available, I’ll pick” | “Do this for me” |
A.2 Resource types
Section titled “A.2 Resource types”- Static resources - fixed URI, e.g.
onlyoffice://templates - Resource Templates - parameterized via RFC 6570 URI templates, e.g.
onlyoffice://templates/{id}/schema. Client lists them viaresources/templates/list, expands the URI, thenresources/reads it. - Subscriptions (optional) - client sends
resources/subscribefor a URI; server pushesnotifications/resources/updatedwhen it changes. Useful for live data.
A.3 Two optional sub-capabilities
Section titled “A.3 Two optional sub-capabilities”{ "capabilities": { "resources": { "subscribe": true, "listChanged": true } } }subscribe- per-resource change notificationslistChanged- server emitsnotifications/resources/list_changedwhen the resource set itself changes (e.g. a new template gets added)
Both are independent; servers may support neither, either, or both.
A.4 Content shapes
Section titled “A.4 Content shapes”// Text{ "uri": "file:///README.md", "mimeType": "text/markdown", "text": "..." }
// Binary (base64){ "uri": "file:///logo.png", "mimeType": "image/png", "blob": "iVBORw0KG..." }Annotations (audience "user"/"assistant", priority 0.0-1.0, lastModified ISO 8601) let clients filter and rank.
A.5 Standard URI schemes
Section titled “A.5 Standard URI schemes”https://- client fetches directly from the web (server doesn’t proxy)file://- filesystem-like; may use XDG MIME types (e.g.inode/directory)git://- version control- Custom schemes (must be RFC 3986 compliant) - e.g.
onlyoffice://
A.6 Where resources are useful
Section titled “A.6 Where resources are useful”| Scenario | Why a resource beats a tool |
|---|---|
| File/context picker UI | Host shows a tree of file:// URIs; user picks; host reads and injects. No model round-trip needed. |
| Schema discovery | Client lists onlyoffice://templates/{id}/schema, expands, reads schema before calling generateDocument - lets the host render a form. |
| Live data feeds | resources/subscribe to db://users/active; server pushes updates when the underlying data changes. No polling. |
| Config / reference data | Templates, credentials metadata, DB schemas - read-once context that doesn’t change per request. |
| Generated artifacts | After a tool produces output, expose it at onlyoffice://documents/{id} so the host can preview/download without re-invoking the tool. |
| Audit/history | log://session/12345 resources let the host show what happened without exposing a “get history” tool the model could abuse. |
A.7 Verified OnlyOffice resource inventory
Section titled “A.7 Verified OnlyOffice resource inventory”Tested via stdio resources/list against node dist/index.js:
onlyoffice://templates [application/json] staticonlyoffice://templates/stellenbeschreibung/schema [application/json] templateonlyoffice://templates/dienstliches_schreiben/schema [application/json] templateonlyoffice://templates/beschlussvorlage/schema [application/json] templateonlyoffice://templates/praesentation/schema [application/json] templateonlyoffice://templates/bericht/schema [application/json] templateonlyoffice://templates/worms_beschlussvorlage/schema [application/json] templateonlyoffice://templates/worms_kopfbogen_intern/schema [application/json] templateonlyoffice://templates/worms_stellenausschreibung/schema [application/json] templateonlyoffice://templates/worms_arbeitsplatz_bewertung/schema [application/json] templateonlyoffice://templates/worms_amtliches_anschreiben/schema [application/json] templateonlyoffice://templates/worms_presentation/schema [application/json] templatePlus a ResourceTemplate onlyoffice://documents/{id} for generated artifacts (listed dynamically from the artifact store).
Reading one (onlyoffice://templates/stellenbeschreibung/schema) returns the JSON schema the admin UI uses to render the fill-form - exactly the “schema discovery before tool call” pattern from the table above.
A.8 OnlyOffice capability advertisement
Section titled “A.8 OnlyOffice capability advertisement”{ "resources": { "listChanged": true } }listChanged: true- server will notify when templates are added/removed (admin CRUD)subscribeis not advertised - clients cannot subscribe to individual template changes. Gap if you want live schema updates when an admin edits a template.- Resources are stdio-only - the HTTP entry point does not register them (per
src/index.ts:95,src/server.tshas noregisterResourcecalls). HTTP clients see-32601 Method not foundonresources/list.
A.9 Why OnlyOffice uses resources (design rationale)
Section titled “A.9 Why OnlyOffice uses resources (design rationale)”The three registrations map to three real workflow needs:
onlyoffice://templates- host app can render a template browser UI without callingonlyoffice_list_templatestool. The model doesn’t need to be in the loop for browsing.onlyoffice://templates/{id}/schema- host app reads the schema, renders a fill-form (TemplateFillDialog.vuein the admin UI does exactly this), submits viagenerateDocumenttool. The resource decouples “what fields exist” from “generate the doc”.onlyoffice://documents/{id}- after generation, the artifact is exposed as a URI so the host can preview/download without re-invoking the tool.
This is the textbook MCP resource pattern: application-curated context that flows through the host, not through model tool calls.