feat(mcp): migrate MCP client from nanobot to official modelcontextprotocol/go-sdk #5
No reviewers
Labels
No labels
area/engine
clarity/clear
enhancement
security
size/M
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
basepeak-ai/gptscript!5
Loading…
Reference in a new issue
No description provided.
Delete branch "chore/mcp-go-sdk-migration"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 1 of basepeak-ai/bpai#777: replace the nanobot-based MCP client with the official
github.com/modelcontextprotocol/go-sdkv1.7.0.What changed
pkg/mcp/loader.go: sessions now connect via SDK transports —CommandTransport(stdio),StreamableClientTransport(HTTP, standalone SSE stream disabled to match previous request/response-only behavior), with an explicit fallback to the legacySSEClientTransportfor pre-streamable servers. Auth failures (401/403) surface as-is and never trigger the fallback, keeping the "unauthorized" marker bpai's reactive OAuth connect flow matches on.tools/listnow follows pagination cursors.pkg/mcp/runner.go:tools/callviaClientSession.CallTool; the model-facing result JSON is pinned to the previous client's exact shape (isErroralways present,contentomitted when empty, no_meta) via a dedicated wire struct + snapshot test.ServerConfig.Headersare injected through anhttp.ClientRoundTripper — the path bpai uses for per-user bearer tokens. (${VAR}expansion was a no-op in the old client — it evaluated against a ClientOption env the fork never set — so headers keep passing verbatim.)pkg/mcp/client.godeleted:Local.Client/Capabilitieshad zero callers in this repo and in bpai.supervisere-exec removed (_execbranch inpkg/cli/main.go,NANOBOT_BINin tests); stdio servers are spawned directly by the SDK with a PATH/HOME/USER env allowlist plus the configured env, matching the previous behavior.ShutdownServernow hashes the config withAllowedToolscleared (matchingLoadTools), so shutdowns of configs carryingallowedToolsno longer silently no-op;Local.Closereports close errors, clears the session map, and cancels the session context first so shutdown fails fast on dead servers instead of hanging on the session DELETE.git.basepeak.dev/basepeak-ai/nanobotand transitivemcp-oauth-proxy; addsmodelcontextprotocol/go-sdkv1.7.0.Invariants preserved
ServerConfigJSON wire shape andGetBaseURL()precedence (bpai's#!mcpblob contract) — untouched.allowedToolsraw-name filtering — unchanged, existing regression tests still pass.Testing
pkg/mcp: 8/8 pass, including new tests for the wire-format snapshot, legacy-SSE fallback, 401-without-fallback (asserts no unauthenticated GET is attempted), header injection, and theShutdownServerdigest fix.go test ./...: green exceptintegration/andpkg/loader/github, which fail identically on unmodifiedmain(missinggptscript-credential-helpersrepo on this Forgejo instance / network-dependent GitHub fixture) — pre-existing, unrelated.golangci-linton touched packages: no new findings vsmain.Follow-ups (not this PR)
_execsupervise branch + direct nanobot dep from bpai'smain.go(only after this ships in the pinned version).@sherpa review
@sherpa review
CI note: all four jobs (test, integration, validate-docs, check-label) fail with the same signature on the previous PR (
06f6b2d, merged, current main) — inherited upstream plumbing (check-label queries api.github.com from Forgejo; integration needs the missing gptscript-credential-helpers repo; validate-docs is the docusaurus build). Local verification on this branch: pkg/mcp 8/8 (incl. wire-format snapshot, legacy-SSE fallback, 401-no-fallback, header injection, ShutdownServer digest regression test), full go test ./... green except integration/ and pkg/loader/github which fail identically on unmodified main; golangci-lint on touched packages shows no new findings vs main.Reviewed head commit:
52d93918803332d44734022feae5e7b064835c52✅ Looks good to merge
This PR cleanly swaps the nanobot MCP client for
modelcontextprotocol/go-sdkwhile preserving behavior that matters: the stdio env allowlist (PATH/HOME/USER) exactly matches nanobot'scleanOSEnv, the model-facingCallToolResultJSON shape is pinned with a dedicated regression test, and theShutdownServer/Closesession-lifecycle changes (clearingAllowedToolsbefore hashing, canceling the session context before issuing per-sessionDELETEs) are correct against the SDK's actualstreamableClientConn.Closesemantics — I checked the go-sdk source to confirm both the DELETE-on-close and cancel-context-for-fail-fast claims hold. The new tests (legacy SSE fallback, auth-error-skips-fallback, header injection, shutdown cache-key regression) target exactly the tricky edges of this migration. Build andgo test ./pkg/mcp/...pass;go vetis clean on the touched packages.Minor nits (non-blocking):
pkg/mcp/client.go(the exportedLocal.Client()/Clienttype /Capabilities()wrapper) was deleted with no replacement. It's unused anywhere in this repo, and removing it was structurally required once the nanobot types it wrapped went away, but if any sibling repo (e.g. bpai) calledmcp.DefaultLoader.Client(...)directly, that would only surface as a downstream build break — worth a quick confirmation it's been migrated in lockstep.isAuthError's\b40[13]\bfallback could in theory false-match an unrelated "401"/"403" substring in a non-auth error message (e.g. a port number), causing the legacy SSE fallback to be skipped when it shouldn't be. Very low likelihood in practice given the primaryunauthorized/forbiddenstring checks already cover the common case.Re the nits: (1) verified — bpai has zero Go imports of this repo's pkg/mcp (grep over *.go finds only a JSON-compat comment in bpai's own pkg/mcp/mcp.go); its contract is purely the #!mcp JSON blob, so deleting Local.Client/Capabilities can't break it. (2) agreed it's theoretical — a false match only skips the legacy-SSE fallback and surfaces the original streamable error, it never masks anything; the primary unauthorized/forbidden checks carry the real cases, so leaving the regex as-is. Merging per standing policy (sherpa approval + CI baseline documented above).