Skip to content

feat(mcpgen,mcpserver): templated @mcpTool selections#34

Merged
zer0stars merged 1 commit into
mainfrom
feat/mcpgen-templated-selection
Apr 23, 2026
Merged

feat(mcpgen,mcpserver): templated @mcpTool selections#34
zer0stars merged 1 commit into
mainfrom
feat/mcpgen-templated-selection

Conversation

@zer0stars

Copy link
Copy Markdown
Member

Summary

  • @mcpTool(selection: "...") now accepts Go text/template syntax. The rendered selection is spliced into the query at call time with the call's argument map as template data.
  • Non-templated selections keep taking the existing fast path; no breaking change for any existing tool.
  • Adds ToolDefinition.SelectionTemplate and the SelectionPlaceholder marker that mcpgen puts into Query when the selection is templated.

Why

Static selection strings cannot express selection sets that depend on per-call argument values. For consumers like telemetry-api's signals query, where each {name, agg} pair needs to become its own sub-field name(agg: AGG), the current shortcut tool's selection timestamp returns empty buckets and forces models to stuff signal names into unrelated arguments like filter. Fixing this at the mcpgen layer means every consumer repo gets dynamic selections by annotation alone — no schema reshaping, no resolver rewrites, no breaking changes in consumer schemas.

Example

signals(tokenId: Int!, signalRequests: [SignalRequest!]!): [Bucket!]
  @mcpTool(
    name: "get_signals"
    description: "..."
    selection: "timestamp {{range .signalRequests}} {{.name}}(agg: {{.agg}}) {{end}}"
  )

A call with signalRequests: [{name: \"speed\", agg: AVG}, {name: \"rpm\", agg: MAX}] renders to:

query(...) { signals(...) { timestamp  speed(agg: AVG)  rpm(agg: MAX)  } }

Changes

  • pkg/mcpserver/mcpserver.go: ToolDefinition.SelectionTemplate, exported SelectionPlaceholder, error propagation from registerShortcutTools through New.
  • pkg/mcpserver/tools.go: parse each SelectionTemplate once at registration; render + splice per call; fail-fast on bad templates or missing placeholder.
  • cmd/mcpgen/condensed.go: detect {{ in selection; emit SelectionPlaceholder in the generated Query; skip static validation for templated selections.
  • cmd/mcpgen/generate.go: isTemplatedSelection helper; Go file template emits SelectionTemplate when non-empty.
  • Tests:
    • cmd/mcpgen: TestTemplatedSelection, TestTemplatedSelectionGeneratedOutput + testdata/templated_selection.graphqls.
    • pkg/mcpserver: TestSelectionTemplateRendering, TestSelectionTemplateInvalidRejectedAtRegistration, TestSelectionTemplateMissingPlaceholderRejected.

Test plan

  • make lint clean
  • go test ./... (all existing + new tests pass)
  • Bump server-garage in telemetry-api, convert the signals / signalsLatest @mcpTool annotations to templated selections, confirm the /mcp tools return correctly-shaped data end-to-end

Static @mcptool(selection: "...") strings cannot express selection sets
that depend on per-call argument values — for example, a signals query
where the caller names a list of {signal, aggregation} pairs and each
pair becomes its own sub-field `name(agg: AGG)`. Tools of that shape
today return useless data because the generated query hard-codes a
selection that matches none of the requested signals.

Support Go text/template syntax inside the `selection` argument. When
`selection` contains `{{`, mcpgen:

- skips the static field-name validation (rendered fields are dynamic)
- emits the query with a `SelectionPlaceholder` marker where the
  selection would normally go
- stores the raw template on `ToolDefinition.SelectionTemplate`

At registration time mcpserver parses each template once (failing fast
on syntax errors) and on every tool call renders it against the call's
argument map, splicing the result into the query. Non-templated
selections take the existing code path unchanged.

Example:

    signals(...): [SignalAggregations!]
      @mcptool(
        name: "get_signals_time_series"
        description: "..."
        selection: "timestamp {{range .signalRequests}} {{.name}}(agg: {{.agg}}) {{end}}"
      )

The generated tool exposes `signalRequests` as an argument; the model
passes `[{name:\"speed\",agg:AVG}, ...]`; mcpserver renders the selection
set at call time and executes the correctly-shaped GraphQL query.
@zer0stars
zer0stars merged commit 5825515 into main Apr 23, 2026
4 checks passed
@zer0stars
zer0stars deleted the feat/mcpgen-templated-selection branch April 23, 2026 19:45
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