Documentation
¶
Overview ¶
Package mcp is the protocol surface `flow mcp` serves: tool names and descriptions derived from the schema, the RPC dispatch table, and registration of both onto an *mcp.Server.
Split out of cmd/flow's own package by #410: this is the half of the old mcp.go that has no CLI-specific dependency — no flags, no egress policy, no secrets — and is therefore the part with a plausible non-CLI caller (an embedder serving MCP, #380's task-invocation tool, #241's agent surface). What stays in cmd/flow is the two tools that are not RPCs (flowstate_run_local and flowstate_test), because both execute against this binary's own flags — egress, secrets, plugins — which only the command line can supply; see cmd/flow/mcp.go.
The one seam this package needs back from the CLI is redaction: an agent's context is an untrusted-consumer surface exactly like a terminal, so a GetResponse answered here must be narrowed the way `flow get` narrows one, which is a decision cmd/flow owns (--reveal-sensitive). See Deps.
The agent surface is the machine surface, taken seriously ¶
`flow mcp` serves the control plane to a model the way `--output json` serves it to a pipe: the same schema messages, projected. Every WorkflowService RPC becomes one tool, discovered by walking the service descriptor rather than kept in a list — so an RPC added to the schema is a tool the day the code is regenerated, and there is no tool list to fall behind the engine. docs/DSL.md wrote this down as a rule before the surface existed: MCP is generated, not written.
Index ¶
- Constants
- Variables
- func AddCapabilities(srv *mcp.Server, local *server.FlowstateServer, ...)
- func AddLocalCapabilities(srv *mcp.Server, local *server.FlowstateServer, deps Deps, ...)
- func AddTools(srv *mcp.Server, local *server.FlowstateServer, ...)
- func ApprovalCardDigest() string
- func ApprovalCardResourceMeta() mcp.Meta
- func CapErrorMessage(runError *v1.RunResponse_Error) bool
- func DebugTool() *mcp.Tool
- func DropDeclaredOutputs(response *v1.GetResponse) bool
- func FitResult(rungs ...func() ([]byte, error)) ([]byte, int, error)
- func FitResultWithin(limit int, rungs ...func() ([]byte, error)) ([]byte, int, error)
- func NewMessage(md protoreflect.MessageDescriptor) proto.Message
- func NewServer(version string) *mcp.Server
- func ReduceTranscript(outputs *v1.Workflow_StepOutputs) (kept, total int)
- func ReducedTestTool() *mcp.Tool
- func ReducedTranscript(outputs *v1.Workflow_StepOutputs) (*v1.Workflow_StepOutputs, int, int)
- func RunLocalTool() *mcp.Tool
- func SchemaForMessage(md protoreflect.MessageDescriptor) map[string]any
- func ServeTools(ctx context.Context, srv *mcp.Server, local *server.FlowstateServer, ...) error
- func TestTool() *mcp.Tool
- func ToolDescription(rpc string) string
- func ToolError(err error) *mcp.CallToolResult
- func ToolName(rpc string) string
- type Deps
- type ServiceMethod
- type ToolRegistration
Constants ¶
const ( // DSLResourceURI is the Flowfile language reference. DSLResourceURI = "flowstate://docs/dsl" // CatalogResourceURI is what this build can execute. CatalogResourceURI = "flowstate://catalog/tasks" // ExamplePrefix is where a single example is addressed, and // ExampleTemplate is the RFC 6570 template a client expands to reach one. ExamplePrefix = "flowstate://docs/examples/" ExampleTemplate = ExamplePrefix + "{name}" )
const ( MarkdownMIME = "text/markdown" JSONMIME = "application/json" YAMLMIME = "application/yaml" )
MIME types, named once so a resource's declaration and its contents cannot disagree about what was served.
const ( // UIExtension is the MCP Apps extension identifier, as the specification // defines it. It is the key both a client's and a server's capabilities use. UIExtension = "io.modelcontextprotocol/ui" // UIAppMIME is the media type an MCP Apps resource is served as. It is a // profile of text/html, not a media type of its own, which is what lets a // host that does not know the profile still recognise the document. UIAppMIME = "text/html;profile=mcp-app" // ApprovalCardURI is the card's identity. The URI never changes; the // content digest below is what says which revision of it a host is holding. ApprovalCardURI = "ui://flowstate/approval-card" // UIToolMetaKey is the `_meta` member a tool declares its view under, and // UIResourceURIKey the member inside it naming the resource. UIToolMetaKey = "ui" UIResourceURIKey = "resourceUri" UIMIMETypesKey = "mimeTypes" UIContentHashKey = "picatz.github.io/flowstate.contentDigest" UICardResourceKey = "approval-card" )
const DebugToolDescription = "Hold a test case's run at each step and ask the paused run questions: what a step " + "produced, what an expression evaluates to, what is in scope. This is the tool for \"why did " + "that fail\", after " + TestToolName + " has told you that it did.\n\n" + "One call is one session. `commands` is the script that drives it — the run starts held before " + "its first step, each command answers or advances, and when the script runs out the run " + "continues to the end. Nothing here is interactive and nothing waits for a human: submit the " + "questions you have, read the transcript, submit more.\n\n" + "The answer carries the session transcript (every stop, every step's own outcome, every " + "answer), the script that produced it — re-send it with more commands appended to go further — " + "and the case's ordinary verdict, which this tool cannot change: a debugged run is the run, and " + "its expectations are judged exactly as " + TestToolName + " judges them.\n\n" + "`inspect` evaluates CEL against the paused run's own scope, through the engine's own " + "evaluator: it is cost-bounded like every expression in the file, and it can name whatever the " + "file could name at that point (`steps.<id>.<output>`, `inputs`, `vars`, a loop's binding). It " + "cannot resolve a secret — `secret(...)` is compiled into a reference when a workflow is built " + "and is never a function anything calls, so there is nothing here to call.\n\n" + "A case that fails is held open once more after the verdict, its failures printed and the " + "finished run still questionable — so one script can assert, see the failure, and then ask what " + "the run actually produced.\n\n" + "Runs on stubs, like " + TestToolName + ": no egress, no secret resolved, a virtual clock. " + "Debugging a real, unstubbed local run is not this tool."
DebugToolDescription is written for the model deciding whether a failing case is worth a debugger.
It leads with the question this tool answers that no other tool here does — *why* did the case fail — because a model that reads this as "another way to run tests" will keep guessing with log steps instead.
const DebugToolName = ToolPrefix + "debug"
DebugToolName is the step debugger, as a tool (#928 slice 3).
const MaxResultBytes = 256 << 10
MaxResultBytes bounds what any tool on this surface may answer with.
An agent-facing surface is an untrusted-consumer surface: a run's outputs are whatever the submitted workflow chose to produce, and a workflow producing a megabyte of step outputs would otherwise spend it all in a model's context window. v1.MaxRunStateBytes is the wrong number here — it bounds what Temporal can carry, which is nearly two megabytes and has nothing to do with what is useful to read.
Exported because cmd/flow's own two tools (flowstate_run_local, flowstate_test) hold their answers to the identical bound; see ServiceMethod's handler for how the RPC-projected tools enforce it.
const ReducedTestToolDescription = testToolDescriptionOpening + testToolNoRunLocal + testToolDescriptionRest
ReducedTestToolDescription is TestToolDescription with the two paragraphs that compare this tool against flowstate_run_local replaced by the one sentence that is true where that tool is not served: it is not there.
Replaced rather than dropped, because "reach for the other tool afterward" is real advice a model needs an answer to, and silence would leave it looking for one.
const RunLocalToolDescription = "Execute a Flowfile immediately, in this process, with no server and no Temporal, " +
"the same rehearsal `flow run local` performs. Use it to verify a workflow you just authored: " +
"conditions, retries, timeouts, loops, waits and step outputs behave here the way they behave in " +
"production, and the answer is the same document flowstate_get returns for a durable run.\n\n" +
"Fail-closed by default: network egress from `http:` steps is denied and no secret scheme is " +
"registered unless the operator started this server with the flags that permit them " +
"(--egress-policy, --secret-env, --secret-dir, --auth-policy). Nothing in this tool's arguments " +
"can widen that, so a denied request means the server was not configured for it, not that the " +
"workflow is wrong.\n\n" +
"What it does not prove: durability. A local run has no run id, nothing can watch it, it does not " +
"survive this process, Continue-As-New compaction never happens, and parallel steps are rehearsed " +
"rather than genuinely distributed. Submit the compiled specification with flowstate_run when the " +
"rehearsal is right.\n\n" +
"Bounded: `sleep: 24h` is a legal Flowfile, and this call holds this turn open for as long as the " +
"workflow runs, so the operator's --run-local-timeout (default 2m) stops execution and reports the " +
"run as timed out rather than letting an untrusted workflow hold the call forever.\n\n" +
"A source declaring `inputs:` is given them in the `inputs` object of this call, keyed by declared " +
"name and typed as declared; a required one left out, an undeclared name, or a mistyped value is " +
"refused before any step runs. What the source declares under `outputs:` comes back as `runOutputs`.\n\n" +
"Answers with {\"run\": <GetResponse>, \"logs\": [...]}: the run's status, timing and step outputs, " +
"plus whatever `log:` steps emitted. Invalid sources come back as an error carrying positioned " +
"diagnostics (line:column) to correct against."
RunLocalToolDescription is written for the model that has to decide whether this is the tool it wants, and what it will and will not have proved by using it.
const RunLocalToolName = ToolPrefix + "run_local"
RunLocalToolName is the tool an agent calls to execute what it just wrote.
const TestToolDescription = testToolDescriptionOpening + testToolRunLocalComparison + testToolDescriptionRest
TestToolDescription is written for the model choosing between this tool and flowstate_run_local.
Assembled from three parts rather than written as one string, because the middle part is the only one that is not true everywhere: a surface that does not serve flowstate_run_local (see AddLocalCapabilities) must not tell a model to reach for it afterward. Derived rather than duplicated, so the four paragraphs both surfaces share cannot drift into two versions — see ReducedTestToolDescription. Reported by Codex on picatz/flowstate#807.
const TestToolName = ToolPrefix + "test"
TestToolName is the tool an agent calls to rehearse what it just wrote.
const ToolPrefix = "flowstate_"
ToolPrefix namespaces the tools, since a client may aggregate servers.
const WorkflowServiceName protoreflect.FullName = "flowstate.v1.WorkflowService"
WorkflowServiceName addresses the service whose prose this surface reads.
Variables ¶
var LocalTools = map[string]bool{ "Validate": true, "Compile": true, "GetCatalog": true, }
LocalTools names the RPCs that answer in-process rather than over the wire: the server's own handlers take a nil Temporal client, which is the proof — see WorkflowServiceMethods.
var ToolViews = map[string]string{ "Get": ApprovalCardURI, }
ToolViews names the tool each UI resource renders, by RPC.
One entry, and choosing it was the design decision worth writing down. v1.GetResponse is the only answer on this surface that carries both a run's coordinates and its open gates: `progress.pending_waits` reports each parked `wait_for_signal:` with the prompt the author wrote, the signal name that releases it, its deadline and whether it is policed, and `starter` says who asked for the run - which is exactly the set an approval card has to render, and exactly the set a `distinct_from_starter` policy is compared against.
The alternatives do not hold the data. `flowstate_signal` answers with an empty SignalResponse, so a card on it would have nothing to draw and would be a form rather than a view of anything. `flowstate_list` reports many runs and no gates at all: it can say a run is RUNNING and cannot say it is waiting on a person. So the tool that reports a run and its pending gates is Get, and it is the one the card is declared on.
Functions ¶
func AddCapabilities ¶
func AddCapabilities( srv *mcp.Server, local *server.FlowstateServer, remote func() flowstatev1connect.WorkflowServiceClient, deps Deps, extra ...ToolRegistration, )
AddCapabilities is the one registration, shared with the tests so what they exercise is what an agent connects to — two registration sites would be the two-copies defect this repository keeps refinding, on a new surface.
Two halves, and the split is what each is for: tools are the verbs, resources are what an agent reads before choosing one. See resources.go.
func AddLocalCapabilities ¶
func AddLocalCapabilities( srv *mcp.Server, local *server.FlowstateServer, deps Deps, extra ...ToolRegistration, )
AddLocalCapabilities registers only what answers in *this* process: the LocalTools RPCs, plus whatever extra tools the caller supplies, plus the read-only reference resources. It is what `flow mcp serve` — the token-gated HTTP surface, picatz/flowstate#558 — builds its server from.
Three differences from AddCapabilities, each of them a thing that surface must not have:
- No RPC tool that dispatches to a deployment. Those call through a client this process authenticates as *itself*, so serving them to a caller whose own authority nothing here checks yet would make this process a deputy for whoever holds a token. That is why there is no remote parameter to pass: a registration that cannot name a client cannot dispatch to one.
- No UI resources. The one card this surface publishes renders flowstate_get (ToolViews), which is one of the tools above, so mounting it would advertise a view of a tool that is not there.
- Nothing derived from Deps.RemoteCatalogAddress: GetCatalog answers from this binary's own build, which is the only answer available when no deployment is addressed.
func AddTools ¶
func AddTools( srv *mcp.Server, local *server.FlowstateServer, remote func() flowstatev1connect.WorkflowServiceClient, deps Deps, extra ...ToolRegistration, )
AddTools registers one tool per RPC, plus whatever tools the caller passes as extra — flowstate_run_local and flowstate_test, in cmd/flow's own wiring.
func ApprovalCardDigest ¶
func ApprovalCardDigest() string
ApprovalCardDigest names the exact bytes served, in the one spelling this tree uses for that: see v1.ContentDigest, which is the same function the `digest:` pin on a `call:` step is compared against.
The URI is the identity and this is the version. A host caches a resource by URI, so "which card am I holding" is a question only the content can answer, and answering it with a hash rather than a hand-maintained number means the answer cannot be forgotten in a diff that changes the card.
func ApprovalCardResourceMeta ¶
ApprovalCardResourceMeta is the `_meta` served both on the resource's declaration and on its contents, so a host comparing the two cannot find them disagreeing about which revision it has.
No `ui.csp` and no `ui.permissions`. The restrictive default is the point: the card's data arrives on the connection it is already on, its actions leave as tool calls on that same connection, and it needs no origin, no camera, no clipboard and no network of its own. A relaxation asked for "just in case" is a relaxation a host grants.
func CapErrorMessage ¶
func CapErrorMessage(runError *v1.RunResponse_Error) bool
capErrorMessage truncates a run's failure message to [maxReducedErrorBytes], reporting whether it had to.
The cut is made on a rune boundary. A string field carrying invalid UTF-8 is one protojson refuses to marshal at all, so slicing mid-rune would turn a large answer into an encoding error — the failure this ladder exists to replace, arriving by a different door.
func DropDeclaredOutputs ¶
func DropDeclaredOutputs(response *v1.GetResponse) bool
DropDeclaredOutputs removes what a run declared it would answer with, reporting whether there was anything to remove.
Both places, which is the whole reason this is a function rather than one assignment. A run's declared outputs reach a reader either as `GetResponse.run_outputs` or nested inside the transcript arm as `GetResponse.outputs.run_outputs`, and which one a given driver populates is not something a ladder should have to know: the durable path sets the former, while a local run's `localRun` carries the whole `Workflow.StepOutputs` it got back from the engine, declared outputs included.
Missing the nested one is not a small leak — it is the entire declared output, which is the field a single `outputs:` expression can make megabytes wide. It went unnoticed while the transcript rung cleared the oneof outright, because dropping the arm took the nested copy with it; reducing the arm instead rather than clearing it (see the floor's contract above) left the copy behind, and TestTheRunLocalAnswerIsBoundedByItsDeclaredOutputs caught it immediately.
func FitResult ¶
FitResult brings an answer under MaxResultBytes by trying a ladder of progressively smaller documents, and reports which rung it settled on.
This is the one shape all three shrinking answers on this surface share, and it is here because they were three copies of it. `flowstate_run_local` drops logs, then the step transcript, then the declared outputs; `flowstate_test` caps each failure's message, then reduces the report to per-case verdicts; and `flowstate_get` drops the transcript, then a running run's carried state, then the declared outputs. What they *drop* is a property of the message and cannot be shared — a TestReport has no step transcript and a GetResponse has no cases — but the discipline around the dropping is identical, and it is the part with the bug in it if it is written out by hand a fourth time:
- re-encode and re-measure after every rung, because a rung that drops the wrong field is a rung that changed nothing, and a ladder that measures once cannot tell;
- stop at the *first* rung that fits, so an answer loses the least it can rather than everything the ladder knows how to drop;
- return the last rung whether or not it fits, because a document that is still too large is an answer a reader can act on and an empty result is not. Every rung's document parses, so no caller is ever handed JSON cut in half.
The first rung is the untouched answer, so a document already under the bound is returned exactly as it was encoded and rung 0 is reported: a caller can tell "nothing was dropped" from "the first reduction was enough" without comparing bytes.
An encoding error stops the ladder rather than falling through to the next rung. A rung that cannot be encoded is a defect in this surface, not a large answer, and quietly reporting the next-smaller document would hide it.
func FitResultWithin ¶
FitResultWithin is FitResult against a budget smaller than the surface's own, for an answer that will be *embedded* in another one.
The whole difference is who spends the cap. A ladder that fits its document to MaxResultBytes has fitted a *final* answer; wrap that document in anything — one more object, one key, one note saying what was dropped — and the result is over the bound, and every rung of the outer ladder retains the inner document, so the outer floor is oversized by construction and FitResult's own contract hands it back that way (Codex, #1109). The bytes the wrapper needs have to be taken out of the inner budget, because they are not the inner document's to spend.
Callers should compute the reserve rather than guess it: encode the wrapper with the embedded document elided, measure what came back, and pass the remainder. A guessed reserve is a second bound that can be wrong in the direction this exists to prevent.
func NewMessage ¶
func NewMessage(md protoreflect.MessageDescriptor) proto.Message
NewMessage constructs an empty message for a descriptor.
func NewServer ¶
NewServer constructs the server an agent connects to, capabilities and all.
One constructor, shared with the tests, for the reason AddCapabilities is one registration: a second construction is a second set of capabilities, and the one an agent negotiates against would eventually stop being the one the tests negotiate against. The extension declared here is what a host reads to learn that this server serves views at all.
func ReduceTranscript ¶
func ReduceTranscript(outputs *v1.Workflow_StepOutputs) (kept, total int)
reduceTranscript keeps as much of a step transcript as fits in [maxReducedTranscriptBytes], reporting how many of its steps survived.
Smallest steps first, so a reduced transcript holds as many whole steps as it can rather than one enormous one, and ties are broken by name so that the same run always reduces to the same document — a map's iteration order is not an answer to give a caller twice and have differ.
Every step kept is *real and unmodified*. Nothing is synthesized to stand in for what was omitted: a fabricated entry in a transcript is indistinguishable from a step the workflow actually ran, and a reader cannot be expected to know which of its own steps this surface invented. The count in the note is how the omission is reported instead.
At least one step is always kept when any step can be kept at all, because `step_values` is required and an empty transcript is a document the schema rejects. A step carrying no `named_values` is skipped entirely for the same reason — keeping one would answer with something invalid. If that leaves nothing keepable, the transcript is returned exactly as it arrived: it is not this function's business to repair a document that was already invalid.
func ReducedTestTool ¶
ReducedTestTool declares the tool for a surface that serves no flowstate_run_local — see ReducedTestToolDescription. Identical in every other respect, and deliberately built from the same schema, because the two differ in what they say and never in what they accept.
func ReducedTranscript ¶
func ReducedTranscript(outputs *v1.Workflow_StepOutputs) (*v1.Workflow_StepOutputs, int, int)
ReducedTranscript is ReduceTranscript for a transcript the caller may not mutate: the same selection, answered as a new arm that shares the kept steps' messages and the declared outputs rather than cloning either. The local-run preflight uses it to bound a response *before* anything marshals or clones it, without writing into the response the engine handed over — one selection policy, two spellings, so the two venues cannot drift.
func SchemaForMessage ¶
func SchemaForMessage(md protoreflect.MessageDescriptor) map[string]any
SchemaForMessage renders a message descriptor as a 2020-12 JSON Schema object describing the message's protojson encoding: camelCase names, enums by name, bytes as base64, 64-bit integers as strings.
The projection is bounded by [maxSchemaNodes] and is a pure function of the descriptor: fields are walked in declaration order, so the same descriptor yields the same schema, budget exhaustion included.
func ServeTools ¶
func ServeTools( ctx context.Context, srv *mcp.Server, local *server.FlowstateServer, remote func() flowstatev1connect.WorkflowServiceClient, deps Deps, extra ...ToolRegistration, ) error
ServeTools registers one tool per RPC, plus any extra tools the caller supplies, and runs the server on stdio.
func ToolDescription ¶
ToolDescription is the sentence a model chooses a tool by, read from the schema that declares the RPC.
It used to be a hand-written map here, one entry per RPC, which is the written-twice defect this repository keeps refinding: the schema's service section describes the same RPCs, so every description existed in two places and only one of them moved when the behavior did. The prose now lives in proto/flowstate/v1/service.proto and arrives through protodoc.Method, so a sentence corrected in the schema is the sentence an agent is handed, and there is no second copy to correct. Slice 2 of #424.
The whole comment rather than protodoc.FirstSentence, deliberately. A one-line context needs one line, and this is not one: the sentences that make these tools usable are the ones after the first: keep paging past a short page, prefer Cancel to Terminate, a file that does not compile answers with diagnostics rather than an error. Taking only the first sentence would drop exactly the operational half the old map existed to carry.
Fails closed on an RPC the schema does not document, returning "" so that TestEveryToolHasADescription fails rather than an agent being handed a mute tool.
func ToolError ¶
func ToolError(err error) *mcp.CallToolResult
ToolError reports a failure as the tool's result rather than a protocol error, which is what lets a model read the reason and correct itself.
Types ¶
type Deps ¶
type Deps struct {
// Redact narrows a GetResponse to what this surface may show, the way
// `flow get` narrows a spec-less answer (workflow is always nil here:
// this dispatch has no specification in reach). Required; a nil field
// means nothing is ever withheld.
Redact func(response *v1.GetResponse) *v1.GetResponse
// RemoteCatalogAddress, when non-empty, routes flowstate_get_catalog to
// the deployment named here instead of answering from this binary's own
// build. The caller sets it only when the operator named a deployment
// explicitly — --address or FLOWSTATE_ADDRESS — so the in-process answer
// stays the default an agent gets with nothing else stood up (see
// [LocalTools]).
//
// It is a value rather than a bool so a failure to reach that deployment
// can name it: the tool refuses rather than falling back to the local
// answer, because a silent fallback is the defect this field exists to
// fix, one level up — an answer that looks like the deployment's and
// is not. See remoteCatalogCall.
RemoteCatalogAddress string
// WrapHandler, when set, wraps every tool handler [AddLocalCapabilities]
// registers — derived and caller-supplied alike — with the tool's own
// name in hand.
//
// Read only there, deliberately. It exists for a serving surface with
// several callers at once, where a tool that mutates process-wide state
// for the duration of one call needs that call serialized against every
// other tool that reads the same state — `flow mcp serve`'s guard around
// [v1.DefaultRegistry] (cmd/flow/mcpserve.go) is the whole reason it is
// here. Stdio has one caller and needs none of it, so [AddTools] ignores
// this field and the surface an agent host launches is byte for byte the
// one it always was.
WrapHandler func(tool string, next mcp.ToolHandler) mcp.ToolHandler
// WrapResourceHandler is [WrapHandler] for the read-only half of the
// surface, with the resource's URI in hand.
//
// Separate because the two handler types are: a resource is read through
// [mcp.ResourceHandler], not [mcp.ToolHandler]. It exists for the same
// reason and would be pointless without it — flowstate://catalog/tasks
// answers from [v1.DefaultRegistry] exactly as flowstate_get_catalog
// does, so a guard applied only to tools leaves the identical read
// reachable one request away. Also read only by [AddLocalCapabilities].
WrapResourceHandler func(uri string, next mcp.ResourceHandler) mcp.ResourceHandler
// contains filtered or unexported fields
}
Deps is what registration needs back from the binary that embeds this package — kept to the smallest seam that crosses the boundary; see the package doc for why redaction is the one thing that has to.
type ServiceMethod ¶
type ServiceMethod struct {
Name string
Input protoreflect.MessageDescriptor
Call func(ctx context.Context, local *server.FlowstateServer,
remote func() flowstatev1connect.WorkflowServiceClient, in proto.Message) (proto.Message, error)
}
ServiceMethod is one RPC, as the tool derivation needs it.
func WorkflowServiceMethods ¶
func WorkflowServiceMethods() []ServiceMethod
WorkflowServiceMethods enumerates the service.
The names and shapes come from the descriptor — asserted against it by test, in both directions — while the dispatch is written out, because Go generics cannot rank over connect's typed methods without reflection that would cost more clarity than these lines do. A method added to the service without a row here fails TestEveryRPCIsATool.
type ToolRegistration ¶
type ToolRegistration struct {
Tool *mcp.Tool
Handler mcp.ToolHandler
}
ToolRegistration is one tool this package does not itself derive from the service descriptor — flowstate_run_local and flowstate_test, both supplied by the caller, since both execute against flags only the caller has. See the package doc.