Debugging MCP Servers
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
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.