Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

mcp: trim tool surface, add prompt + lifecycle, expand server_info#651

Merged
jeffreyaven merged 15 commits into
mainstackql/stackql:mainfrom
feature/mcp-updatesstackql/stackql:feature/mcp-updatesCopy head branch name to clipboard
May 16, 2026
Merged

mcp: trim tool surface, add prompt + lifecycle, expand server_info#651
jeffreyaven merged 15 commits into
mainstackql/stackql:mainfrom
feature/mcp-updatesstackql/stackql:feature/mcp-updatesCopy head branch name to clipboard

Conversation

@jeffreyaven

Copy link
Copy Markdown
Member

Summary

PR1 of a two-PR series prepping the MCP server for richer client integration. This PR is purely about shaping the tool surface; PR2 will add server modes, elicitation-based approval, and an audit subsystem.

  • 11 tools (down from 18) + 1 prompt: prune placeholders/duplicates, rename the three query tools, add run_lifecycle_operation, describe_resource, describe_method. Convert prompt_write_safe_select_tool to an MCP prompt registered via Server.AddPrompt.
  • Per-tool rendering: drop the runtime format parameter; new pkg/mcp_server/render exposes RenderTable (uniform multi-row) and RenderKV (sparse / single-record). MCP responses carry both rendered text and a typed DTO.
  • Expanded server_info: adds sql_backend and provider_registry derived from runtime context. Drops redundant name/info. Renames Go field IsReadOnly -> ReadOnly (JSON tag is_read_only preserved).
  • Read-only gate: run_mutation_query and run_lifecycle_operation refuse when Config.Server.IsReadOnly is true.
  • New EnabledPrompts allowlist mirroring EnabledTools (independent).
  • Delete orphaned pkg/presentation package - sole consumer was the removed MCP renderer.

Tool surface (final)

Tool Renderer Notes
server_info KV now reports sql_backend, provider_registry, is_read_only
list_providers Table
list_services Table requires provider
list_resources Table requires provider, service
list_methods Table requires provider, service, resource
describe_resource KV new; replaces describe_table
describe_method KV new shape: {provider, service, resource, method}, always EXTENDED
validate_select_query KV renamed from validate_query_json_v2
run_select_query Table renamed from query_json_v2
run_mutation_query KV renamed from exec_query_json_v2; refused in read-only
run_lifecycle_operation KV new; routes through Backend.ExecQuery like mutations but distinct contract for EXEC semantics; refused in read-only

Prompt: write_safe_select (static text from ExplainerPromptWriteSafeSelectTool).

Tools removed

greet, db_identity, query_v2, query_v3, query_validate_json_v2, list_tables_json, list_tables_json_page, describe_table, get_foreign_keys, find_relationships, prompt_write_safe_select_tool (now a prompt).

Breaking changes

  • describe_method input changed from {method_path, extended} to {provider, service, resource, method}. Always EXTENDED now.
  • QueryResultDTO is now {rows} (the format, row_count, raw, warnings fields are gone).
  • ValidationResultDTO is now {valid, errors} (the message field is gone).
  • ServerInfoDTO drops name/info, adds sql_backend/provider_registry.

Design notes for reviewers

  • pkg/mcp_server package invariant preserved: zero imports from internal/... or any-sdk. All data crossing the boundary is primitive types or interfaces. The new pkg/mcp_server/render is also pure.
  • ServerBuildInfo follows the data-carrier rule in AGENTS.md: unexported interface (serverBuildInfo), unexported implementation, single exported constructor (NewServerBuildInfo). cmd-side callers use := to bind the inferred unexported type — Go is fine with this.
  • SQLBackend / ProviderRegistry plumbing: cmd/mcp.go decodes runtimeCtx.SQLBackendCfgRaw via dto.GetSQLBackendCfg (uses SQLDialect/DBEngine) and runtimeCtx.RegistryRaw via a small YAML probe (url then localDocRoot). Both are best-effort: empty string if the blob is missing or malformed.
  • JSON wire compatibility: the is_read_only JSON tag is preserved on the renamed Go field so existing robot assertions continue to pass.
  • Out of scope (PR2): no Mode enum, no classifyQuery helper, no four-mode gate logic, no req.Session.Elicit, no AuditSink, no audit middleware. The existing IsReadOnly *bool is the sole gate, wired at the two write-tool handlers.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • golangci-lint run on ./pkg/mcp_server/..., ./internal/stackql/mcpbackend/..., ./internal/stackql/cmd/... -> 0 issues
  • go test --tags sqlite_stackql -count=1 ./... -> all packages pass

Unit tests added

13 new tests in pkg/mcp_server/tools_test.go exercise every tool end-to-end via mcp.NewInMemoryTransports:

  • TestTool_ServerInfo_RendersKVAndStructured
  • TestTool_ListProviders_RendersTable
  • TestTool_ListServices_ForwardsHierarchy
  • TestTool_DescribeResource_UsesKVRenderer
  • TestTool_DescribeMethod_RequiresFourSegments
  • TestTool_ValidateSelectQuery_SuccessAndFailure
  • TestTool_RunSelectQuery_ForwardsRowLimit
  • TestTool_RunMutation_RefusedInReadOnly
  • TestTool_RunLifecycleOperation_PositiveAndReadOnly
  • TestRegistration_EnabledToolsFilters
  • TestPrompt_WriteSafeSelect_RegisteredAndReturnsCanonicalText
  • TestPrompt_EnabledPromptsFilters
  • Plus TestIsToolEnabled/TestIsPromptEnabled for allowlist semantics
  • Plus four renderer tests in pkg/mcp_server/render/render_test.go

Robot scenarios

Renamed (tool action only) in surviving scenarios:

  • validate_query_json_v2 -> validate_select_query
  • query_json_v2 / query_v2 -> run_select_query
  • exec_query_json_v2 -> run_mutation_query

Updated:

  • MCP HTTP Server Info Includes Version also asserts sql_backend + provider_registry.
  • MCP HTTP Server Restricted Tools Allowlist keyed on server_info (fixture changed from ["greet"] to ["server_info"]).

New:

  • MCP HTTPS Describe Resource Canonical
  • MCP HTTPS Describe Method Canonical — asserts a row with name == 'project' via list comprehension
  • MCP HTTPS Run Lifecycle Operation Canonical — positive path against aws.transfer.servers.stop_server
  • MCP HTTP Read Only Server Info Flag — asserts is_read_only: true against the new 9916 server
  • MCP HTTPS Run Mutation Refused In Read Only — refusal for both write tools

Start MCP Servers extended with a fifth server on 127.0.0.1:9916 with read_only: true.

Removed 8 obsolete scenarios (Greet, DB Identity, Query V3, Foreign Keys, Find Relationships, Describe Table, Greeting Tool, Show Version SQL — the latter superseded by the expanded server_info).

  • Robot suite run locally / in CI

🤖 Generated with Claude Code

jeffreyaven and others added 15 commits May 14, 2026 23:38
- Extract buildVersionShowOutput helper to drop NewShowInstructionExecutor
  back under gocyclo 30.
- Extract resolveInitialHIDs helper to drop GetHIDs back under cyclop 30.
- Robot test: SHOW VERSION EXTENDED is not in the parser grammar; the
  correct form is SHOW EXTENDED VERSION (extended_opt precedes the id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The VERSION case added in the WIP MCP work pushed
NewShowInstructionExecutor's cyclomatic complexity over the linter's
ceiling of 30. Extracting only the VERSION case left it at 31 -- still
one over. Pull the INSERT case into its own helper as well to bring
the function back under the limit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Show Version / Show Version Extended tests asserted that stdout
contained \${EXPECTED_SEMVER}, derived from BUILDMAJORVERSION /
BUILDMINORVERSION / BUILDPATCHVERSION. The build pipeline sets these
for the binary but the patch component is the github.run_number, which
changes every build, and the env vars don't propagate consistently into
the robot harness Python step. Result: stdout shows 0.10.444 but the
assertion compares against the 1.1.1 default and fails.

Switch to a generic semver regex (\d+\.\d+\.\d+) that matches any
build's version cell. The column-header checks (version, commit,
build_date, platform) are kept as literal substring checks.

Also fix two SHOW VERSION EXTENDED occurrences in mcp.robot; the parser
grammar is SHOW extended_opt id_or_var, so the correct form is
SHOW EXTENDED VERSION (already fixed in stackql_from_cmd_line.robot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add BuildInfo interface with getter methods
- Create private buildInfo struct implementing the interface
- Add NewBuildInfo constructor for mutation only at creation
- Maintain backward compatibility with legacy global variables
- Update cmd/root.go init() to use new constructor pattern

Co-authored-by: Jeffrey Aven <jeffreyaven@users.noreply.github.com>
- Replace global variable with thread-safe singleton pattern using sync.Once
- Add missing periods to all comments (godot linter)
- Fix import formatting (goimports)
- Maintain private struct/public interface pattern
- Preserve backward compatibility

Co-authored-by: Jeffrey Aven <jeffreyaven@users.noreply.github.com>
…globals

- Remove singleton pattern with sync.Once variables
- Remove SetDefault/GetDefault functions that used global state
- Add NewBuildInfoFromLegacy() helper for accessing legacy globals
- Maintain private struct/public interface pattern
- Keep only legacy variables with lint suppressions for -ldflags compatibility

Co-authored-by: Jeffrey Aven <jeffreyaven@users.noreply.github.com>
Co-authored-by: Jeffrey Aven <jeffreyaven@users.noreply.github.com>
- buildinfo: drop the legacy package-level globals (BuildMajorVersion,
  ..., SemVersion). Public surface is now BuildInfo interface +
  NewBuildInfo constructor + Init/Get accessor backed by a sync.Once
  singleton, so the struct is private and mutation only happens through
  the constructor.

- cmd/root.go: fold the buildinfo publication into the existing init()
  rather than adding a second one. SemVersion package var is gone;
  rootCmd.Version and the version template are set from buildinfo.Get()
  inside init(), which runs after -ldflags has populated the cmd.Build*
  link-time targets.

- mcpbackend: ServerBuildInfo was a public mutable struct with no serde;
  collapse it to an unexported serverBuildInfo interface constructed via
  newServerBuildInfo(buildinfo.BuildInfo, transport). The backend
  constructors now take a BuildInfo + transport string and build the
  private value internally, so there is no mutation surface outside the
  constructor.

- stackql_context.py / stackql_from_cmd_line.robot: drop the four
  DESCRIBE_METHOD_* shared constants; queries are inlined in the robot
  scenarios per reviewer guidance.
# Conflicts:
#	internal/stackql/cmd/mcp.go
#	internal/stackql/mcpbackend/mcp_reverse_proxy_backend_service.go
#	internal/stackql/mcpbackend/mcp_service_stackql.go
#	pkg/mcp_server/backend.go
#	pkg/mcp_server/dto/dto.go
#	pkg/mcp_server/example_backend.go
#	pkg/mcp_server/server.go
#	test/robot/functional/mcp.robot
docs/mcp.md still documented the renamed/removed tools (query_json_v2,
validate_query_json_v2, exec_query_json_v2, meta_describe_table) in
both the canonical bash invocations and the worked sample-output
section. Rewrite the tool-list table around the current 11 tools + 1
prompt, refresh the bash examples to use the new tool names and the
describe_method four-segment input, add read-only-server invocation,
and slim the trailing sample-output block to one representative
response per tool against the new {rows}/{valid}/{messages,timestamp}
shapes.

Also delete two dead commented-out blocks that survived the refactor:
the TestConfigValidation stub in server_test.go and the
field-by-field validation skeleton inside Config.Validate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The new server returns rendered markdown in Content and the typed DTO in
StructuredContent.  CallToolText previously concatenated Content blocks
verbatim, so scripting clients that called json.loads on stdout (every
robot scenario that uses --exec) saw markdown instead of JSON and failed
to parse.

CallToolText now prefers StructuredContent (re-marshalled as compact
JSON), falling back to Content text only when no structured payload is
present.  Tool-level errors (IsError == true) are returned as a Go error
containing the text payload so the CLI exits non-zero with the refusal
message on stderr - which is how the read-only refusal scenarios already
assert behaviour.

The result-shaping logic is extracted to a pure formatToolResult helper
so it can be unit-tested without an HTTP transport.  Five new tests
cover: StructuredContent preferred, text fallback, IsError -> error,
nil result, array-shaped structured content.

Also fix the run_lifecycle_operation positive robot scenario: the
previous EXEC used aws.transfer.servers.stop_server with an embedded
@@JSON='{...}' body whose nested quotes collided with robot's escape
processing and yielded malformed JSON args.  Switch to the simpler
aws.ec2.instances.instances_Start canonical lifecycle EXEC (same
"The operation was despatched successfully" response) with no embedded
JSON body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssertion

Two real failures surfaced in CI:

1. MCP HTTPS Describe Method Canonical -- describe_method's `shape`
   column carries an inner JSON-encoded string per row, so the wire
   payload contains escaped quotes (\"description\":\"...\").  The
   Parse MCP JSON Output helper interpolated the captured stdout into
   a triple-quoted Python source literal:

       Evaluate    json.loads('''${input}''')    json

   Python then re-interpreted the \" sequences inside the literal,
   yielding malformed JSON that json.loads choked on.  Switch to
   passing the value through Robot's variable namespace ($input) so
   the bytes reach json.loads unchanged.

2. MCP HTTPS Run Lifecycle Operation Canonical -- the scenario asserts
   on key 'messages', but the reverse-proxy backend at port 9004
   returns ExecQuery results from database/sql -> {timestamp,
   rows_affected?, last_insert_id?}.  Only the orchestrator-backed
   primary backend returns {messages, timestamp}.  Both shapes carry
   `timestamp`; relax the assertion to that common floor and document
   the per-backend contract in code comments on both ExecQuery methods.
   The MCP HTTPS Server Exec Query Canonical scenario (which already
   passes against 9004) only asserts `timestamp` for the same reason.

Also add a focused unit test
(TestFormatToolResult_EmbeddedJSONStringInValueRoundtripsCleanly) that
proves the client formatter's output round-trips through json.Unmarshal
when a structured value contains a JSON-encoded string -- the strict-
mode equivalent of what Robot does after the helper fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeffreyaven jeffreyaven merged commit 0374740 into main May 16, 2026
20 checks passed
@jeffreyaven jeffreyaven deleted the feature/mcp-updates branch May 16, 2026 07:15
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.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.