Skip to content

Migrate MCP SDK: mark3labs/mcp-go -> official go-sdk via new abstraction layer - #8

Open
LackOfMorals wants to merge 7 commits into
mainfrom
move-to-official-mcp-go-sdk
Open

Migrate MCP SDK: mark3labs/mcp-go -> official go-sdk via new abstraction layer#8
LackOfMorals wants to merge 7 commits into
mainfrom
move-to-official-mcp-go-sdk

Conversation

@LackOfMorals

Copy link
Copy Markdown
Member

Summary

  • The MCP spec and its SDKs move fast, including breaking changes, and this codebase had zero abstraction around mark3labs/mcp-go — SDK types were spelled out directly in the server, every tool spec/handler, and every test tier.
  • Introduces internal/mcpsdk as the single package allowed to import an MCP SDK (plus internal/mcpsdk/mcpsdktest, a matching client wrapper used only by this repo's own test suites), and migrates the server, all five tools, and every test tier onto it.
  • mark3labs/mcp-go is fully removed from go.mod; the server and tools are now backed by the official github.com/modelcontextprotocol/go-sdk, which was already an indirect dependency of this repo.
  • Fixes 4 pre-existing golangci-lint findings (ElementId -> ElementID naming in internal/database/json_tagged_values.go) found while linting the branch.
  • Adds .github/workflows/test.yml to actually run the unit/integration/e2e suites in CI — previously no workflow ran tests at all.
  • Adds a Changie fragment (.changes/unreleased/mcp-sdk-abstraction.yaml) documenting the change for the next release's changelog.

Two real behavioral gaps were found and fixed as part of the migration (not just mechanical renames):

  • The official SDK's stdio transport returns an error (rather than nil) when stdin is closed/exhausted, which broke stdio-mode tests that relied on the old SDK swallowing that case — mcpsdk.Server.ServeStdio now treats a closed/EOF stdin as a clean shutdown, matching prior behavior.
  • CallToolRequest.BindArguments could panic on a nil Params — hardened with a nil check.

Test plan

  • go build ./..., go vet ./..., go test ./... all pass
  • golangci-lint run reports 0 issues
  • go build -tags=e2e ./test/e2e/... and go build -tags=integration ./test/integration/... compile cleanly
  • task test:int / task test:e2e against a real Neo4j (needs Docker for testcontainers) — not run in this environment, recommended before merge
  • task build produces a working binary (--version reports correctly)

🤖 Generated with Claude Code

Jonathan Giffard and others added 2 commits September 10, 2026 23:12
…w abstraction layer

The MCP spec and its SDKs move fast, including breaking changes, and this
codebase had zero abstraction around mark3labs/mcp-go: SDK types were spelled
out directly in the server, every tool spec/handler, and every test tier.
Introduces internal/mcpsdk as the single package allowed to import an MCP SDK,
and migrates the server, all tools, and all tests onto it, backed by the
official github.com/modelcontextprotocol/go-sdk instead of the community
mark3labs/mcp-go (which is now fully removed from go.mod).

Also fixes 4 pre-existing golangci-lint findings (ElementId -> ElementID
naming in internal/database/json_tagged_values.go) and adds a CI workflow
that actually runs the test suites, since none previously did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…test

TestServerLifecycle polled a fixed 4-second wall-clock window for s.Start()
to fail, without ever waiting on the goroutine that ran it. The "invalid
host"/"invalid database" cases only resolve once the driver's own
connection/DNS-resolution attempt times out, which can take noticeably
longer over a CI runner's network than on a developer machine, so the test
would see a false nil at the 4s mark. Switched to a channel-based wait so the
error cases resolve as soon as Start() actually returns, with a generous 30s
safety net for the cases that require a real network timeout to elapse.

This is unrelated to the MCP SDK migration itself (this file has no MCP SDK
dependency) — it's a pre-existing flake that was never exercised in CI until
the previous commit added a workflow that actually runs the test suites.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LackOfMorals

Copy link
Copy Markdown
Member Author

Root-caused the CI failure: TestServerLifecycle in test/integration/server_test.go polled a fixed 4s window for s.Start() to fail rather than waiting on the goroutine itself. The invalid-host/invalid-database cases only resolve once the driver's connection/DNS-resolution attempt actually times out, which can take longer over the CI runner's network than locally — so the test saw a false nil at the 4s mark. This file has no MCP SDK dependency; it's a pre-existing flake that had never been exercised in CI until this PR added a workflow that actually runs the test suites. Fixed in 82e2fe5 by waiting on Start()'s real result via a channel, with a 30s safety net for the network-timeout cases. Note the failing integration step also meant the e2e step never got a chance to run in that CI attempt — worth re-checking once this re-runs.

Jonathan Giffard and others added 2 commits September 10, 2026 23:27
…ycle test

The previous fix's 30s external wait was racing the neo4j driver's own
default MaxTransactionRetryTime (also 30s): a bad host is classified as a
transient/retryable ConnectivityError, so ExecuteReadQuery quietly retried
for its full default budget before ever returning, and lost the race against
this test's timeout in CI (confirmed in the PR's CI run: "expected Start() to
fail within 30s, but it did not return").

Configure the test's driver with a 3s MaxTransactionRetryTime / 2s
SocketConnectTimeout instead of padding the external wait further. Verified
locally against an unreachable host: this surfaces the failure in ~6s
(driver retry + backoff overhead), well inside the now-reduced 15s external
wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n test

The read-only-mode e2e assertions (3 tools / 4 tools) predate the
give-feedback tool, added 2026-08-19 in 5ff9d24, but this e2e test's counts
were never updated to include it. give-feedback is read-only, so it belongs
in both counts: 4 for read-only mode (get-schema, read-cypher,
list-gds-procedures, give-feedback) and 5 with all tools enabled (adding
write-cypher) — matching the counts internal/server/tool_register_test.go
already asserts. Unrelated to the MCP SDK migration; a pre-existing staleness
that had never been exercised in CI until this PR's new test workflow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LackOfMorals

Copy link
Copy Markdown
Member Author

CI is now green (run https://github.com/neo4j-labs/neo4j-mcp-canary/actions/runs/34537845656) — unit, integration, and e2e all pass. Two more pre-existing issues surfaced and fixed along the way, both unrelated to the MCP SDK migration itself (neither file has an MCP SDK dependency), just never exercised in CI until this PR added a workflow that runs the suites:

  1. test/integration/server_test.go's invalid-host/invalid-database case raced the neo4j driver's own default 30s MaxTransactionRetryTime (an unreachable host is classified as a transient/retryable error, so the driver quietly retries for its full default budget) — fixed by giving that test's driver a short 3s retry budget instead of padding the external wait further (verified locally: failure now surfaces in ~6s).
  2. test/e2e/server_initialization_test.go's read-only-mode tool-count assertions (3/4) predated the give-feedback tool (added 2026-08-19) and were never updated — corrected to 4/5 to match what internal/server/tool_register_test.go already asserts.

Jonathan Giffard and others added 3 commits September 10, 2026 23:58
… releases

CheckMinimumVersion rejected every bare classic-versioned release string
(e.g. "5.26", "5.27") outright, regardless of how new it was, unless it
carried Aura's "-aura" suffix. The correct requirement is a plain floor of
5.26 on the classic major.minor version — the suffix has no bearing on it.
5.26 is Neo4j's last classic-versioned release and an LTS.

Also fixes a regression the merge introduced along the way: the classic
pattern's unconstrained \d+.\d+ would otherwise also match calendar-shaped
strings like "2026.06", so the calendar check now runs first (it's
unambiguous — an exact 4-digit year) to avoid misclassifying a too-old
calendar release as an accepted classic one.

With the gate fixed, add real (non-mocked) integration coverage for
internal/queryapi.Service, which previously only had coverage against
httptest.Server fakes: bump the shared test container to
neo4j:5.26-community and expose its Query API port alongside Bolt, then
exercise the version gate, read-cypher, and write-cypher against it for
real in test/integration/queryapi_test.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ompatible test image

CheckMinimumVersion's classicVersionPattern only matched major.minor (e.g.
"5.26"), but real self-managed servers report a full patch version (e.g.
"5.26.30") — the exact string the CI-run container's discovery endpoint
returned. Added the same optional patch group calendarVersionPattern already
has.

Separately: running the new Query API integration tests against a real
neo4j:5.26-community container in CI revealed every request failing with
406 Not Acceptable. Root cause: github.com/neo4j-contrib/query-go-sdk v0.6.0
(our pinned, and currently newest published, dependency) hardcodes its
typed-JSON media type to "v1.1", which per Neo4j's Query API changelog was
introduced in the 2025.11 calendar release — a self-managed classic release
like 5.26 predates that and doesn't recognize it, independent of the version
gate. The gate's 5.26 floor is still correct (this is a client wire-protocol
limitation, not a statement about what Neo4j itself supports), but the
integration test container needs to be wire-compatible with our current SDK
pin, so it's switched to neo4j:2026.07-community, which does support v1.1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r to 5.26

Correction to the previous fix: the "-aura" suffix requirement wasn't the
bug — dropping it was. A bare classic version (self-managed, no "-aura"
suffix) must still be rejected regardless of how new it is, since
self-managed classic servers predate the typed-JSON media type version
query-go-sdk depends on (confirmed by the 406s the previous fix's own CI run
produced against a real neo4j:5.26-community container). What was
genuinely wrong was the floor number: classic Aura versions require at
least 5.26-aura, not 5.27-aura as previously coded.

Restores classicAuraVersionPattern's suffix requirement, lowers
minClassicAuraMajor/Minor to 5/26, and updates version_test.go, README.md,
and the changelog fragment to match. No changes needed to the integration
test container (already calendar-versioned for wire-compatibility reasons
unrelated to this suffix rule) or to test/integration/queryapi_test.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant