Documentation
¶
Overview ¶
Package mcp exposes the use case catalogue to agents (ai-first.md §1.1, ADR-0012).
The tools are generated from core/application/usecase.Registry rather than written out here. That is the whole design: a hand-maintained second list is a list that falls behind, and an agent interface that can do less than the API is the failure this project set out to avoid. A new use case is a new tool the moment it is registered - no code in this package changes.
Index ¶
- Constants
- func InputSchema(fields []usecase.Field) map[string]any
- func PromptMessages(prompt port.Prompt, arguments map[string]string) ([]map[string]any, error)
- func PromptName(prompt port.Prompt) string
- func URIOf(segment string, id shared.ID) string
- type Annotations
- type Catalogue
- type ErrPromptArguments
- type ErrURI
- type PromptArgument
- type PromptSummary
- type Prompts
- type Resource
- type ResourceKind
- type ResourceTemplate
- type Server
- type Sessions
- type StreamSignals
- type Tool
- type Wakeups
Constants ¶
const MimeType = "application/json"
MimeType is what every resource in this server answers as. One type for all of them, because every one of them is a use case result rendered the way `tools/call` renders one - a client that can read a tool result can read a resource.
const Path = "/mcp"
Path is where the server is mounted. Named here rather than in the composition root, because the endpoint is part of what this package promises an agent (api-guidelines.md §2).
const ProtocolVersion = "2025-06-18"
ProtocolVersion is the revision of the Model Context Protocol this server speaks. It is answered on initialize, so a client can decide whether it understands us.
const Scheme = "hubtask://"
Scheme is the URI scheme this server addresses its content under (ai-first.md §1.1).
const SessionHeader = "Mcp-Session-Id"
SessionHeader is where MCP carries the session identifier, in both directions.
Variables ¶
This section is empty.
Functions ¶
func InputSchema ¶
InputSchema turns the declared fields into JSON Schema.
`additionalProperties: false` mirrors what the catalogue does with an input: an unknown field is refused, not ignored. An agent that sees the schema can then correct itself before the call rather than after a silent misplacement (ai-first.md §1.2).
func PromptMessages ¶
PromptMessages renders one prompt and its arguments into the sequence a client sends to a model.
One rule, and no per-prompt program behind it: **the instruction is the system message, and every declared argument the caller filled in becomes one user message after it**, in the order the prompt declared them. That is `Prompt.Ask` generalised from one argument to several, and it keeps the same guardrail - the instruction and the material are two different messages, and nothing in this package concatenates them (ai-first.md §1.3). A prompt cannot be written whose material ends up in the system message, because there is no code path that would put it there.
A resource argument becomes a link rather than content. The client resolves it through `resources/read`, which is where the permission is asked (ADR-0051) - so a prompt naming a collection cannot hand out a collection the caller may not open, and this server reads nothing while rendering a prompt.
func PromptName ¶
PromptName is how a prompt is addressed: `<id>@<version>`.
Types ¶
type Annotations ¶
type Annotations struct {
ReadOnlyHint bool `json:"readOnlyHint"`
DestructiveHint bool `json:"destructiveHint"`
}
Annotations are the hints a client uses to decide whether to ask for confirmation before it calls (ai-first.md §1.1). Destructive operations are the ones an agent token may not even hold by default (§1.3).
type Catalogue ¶
type Catalogue interface {
All() []usecase.Descriptor
ByMCPTool(tool string) (usecase.Descriptor, bool)
Invoke(ctx context.Context, name string, actor appshared.ActorContext, in usecase.Input) (usecase.Output, error)
}
Catalogue is the slice of the use case registry this server needs.
type ErrPromptArguments ¶
type ErrPromptArguments struct{ Reason string }
ErrPromptArguments is a call this server cannot render into a message sequence.
A validation failure rather than a protocol one, and reported as `codeInvalidParams` for the reason an unknown tool is: the caller can correct it, and the correction is in the message.
func (ErrPromptArguments) Error ¶
func (e ErrPromptArguments) Error() string
type ErrURI ¶
type ErrURI struct{ Reason string }
ErrURI is what a URI this server cannot address answers with.
A protocol failure rather than a refusal, and the distinction is the one MCP asks for: an unknown scheme or an unaddressable segment is the client having called wrongly, while a resource the actor may not read is the server saying no - and only the second is worth reporting to the person the agent works for. Which is why this error never travels: `resources/read` turns it into a JSON-RPC error, and a refused read into a result.
type PromptArgument ¶
type PromptArgument struct {
Name string `json:"name"`
Description string `json:"description"`
Required bool `json:"required"`
}
PromptArgument is one entry of a prompt's declared arguments in `prompts/list`.
type PromptSummary ¶
type PromptSummary struct {
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
Arguments []PromptArgument `json:"arguments"`
}
PromptSummary is one entry of `prompts/list`.
func PromptsOf ¶
func PromptsOf(prompts []port.Prompt) []PromptSummary
PromptsOf renders the published prompts, in the store's own order.
The version is not a field of its own, because MCP has none: it is carried in the name, as `weekly-review@v1`. A client that pins a prompt then pins the text, which is the whole point of versioning it - and `prompts/get` accepts the bare name too, so a client that does not care about versions asks for the newest and gets it.
type Prompts ¶
type Prompts interface {
// Published is the prompts written for an agent rather than for this product's own provider.
Published() []aiprovider.Prompt
Get(id string) (aiprovider.Prompt, error)
}
Prompts is the slice of the prompt store this server needs (core/port/ai.Prompts, filtered).
type Resource ¶
type Resource struct {
URI string `json:"uri"`
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType"`
}
Resource is one entry of `resources/list`.
type ResourceKind ¶
type ResourceKind struct {
// Segment is what follows the scheme: `items`, `containers`, `views`.
Segment string
// UseCase is the catalogue name of the read.
UseCase string
// Field is the input the identifier is bound to.
Field string
// Title is what a client shows for the *kind* in `resources/templates/list`. Protocol
// documentation like a tool's description, not display text (ADR-0011): what a person sees in
// their own language is rendered by their own client from their own catalogue.
Title string
// Description says what the content is and what reading it costs.
Description string
// NameField is the key of the read's own output that names one instance. Most resources here
// are named by `name` and an entry by its `title`; the field is in the table rather than
// assumed, so that a kind whose read calls it something else needs no second code path.
NameField string
// ListUseCase is the catalogue read that enumerates this kind, or empty where the kind cannot
// be enumerated. Entries are the empty one, and deliberately so (ADR-0051 decision 3): no
// unanchored item list exists in this product, and one built for `resources/list` would be a
// tenant asking for everything.
ListUseCase string
// Paged says whether that list answers a page with a cursor rather than a bare list.
//
// Exactly one kind may be paged, and `list` relies on it: the MCP cursor is that kind's own
// cursor passed through, and two paged kinds would need a composite cursor this package would
// have to invent and sign itself. A test holds the invariant, so a second one is caught here
// rather than by an agent walking a page twice.
Paged bool
}
ResourceKind is one shape of addressable content: a URI, the catalogue read behind it, and the input field the identifier goes into (ADR-0051 decision 1).
Everything about a resource is here, and nothing about it is anywhere else. `resources/read` parses a URI into one of these and calls `Catalogue.Invoke`; the permission question, the tenant and the audit entry are the application layer's, exactly as they are for a tool call.
func ParseResourceURI ¶
func ParseResourceURI(uri string) (ResourceKind, shared.ID, error)
ParseResourceURI resolves one URI into the read behind it and the identifier to run it with.
The identifier is parsed here, and that is deliberate: an identifier that is not one is a URI this server cannot address, which is a protocol error - while an identifier that is well formed and names nothing the actor may see is the use case's answer, and has to stay one. The difference matters because the second must never confirm existence (ADR-0051 decision 4).
func ResourceKinds ¶
func ResourceKinds() []ResourceKind
ResourceKinds answers the table, for the server and for the tests that hold its invariants.
func (ResourceKind) String ¶
func (k ResourceKind) String() string
String is what a log or a test prints for one kind.
type ResourceTemplate ¶
type ResourceTemplate struct {
URITemplate string `json:"uriTemplate"`
Name string `json:"name"`
Title string `json:"title"`
Description string `json:"description"`
MimeType string `json:"mimeType"`
}
ResourceTemplate is one entry of `resources/templates/list`: the URI shape of content a client cannot enumerate.
It is what makes decision 3 of ADR-0051 workable. `resources/list` does not enumerate entries - there is no unanchored item list in this product and there should not be one - so a client learns `hubtask://items/{id}` from here and constructs the URI itself, from an identifier it got out of `search_items`, `query_items` or a container it has read.
func ResourceTemplates ¶
func ResourceTemplates() []ResourceTemplate
ResourceTemplates renders the table as URI templates, in the table's own order.
type Server ¶
type Server struct {
Catalogue Catalogue
// Prompts is the store the outbound adapters read, published here. Nil is an
// installation running without it, and then the prompts capability is not declared and the two
// methods answer "method not found": a capability is claimed only where something serves it.
Prompts Prompts
// Sessions mints and checks the `Mcp-Session-Id` a handshake hands out. Nil is an
// installation running without the streaming half: no session is issued, none is checked, and
// `GET /mcp` is not served.
Sessions Sessions
// Streams bounds the server-initiated connections, and it is the *same* registry the change
// stream uses: an agent's stream is not a different kind of connection from a browser's and
// must not have a different kind of limit.
Streams *stream.Registry
// Wakeups tells an open stream that a workspace changed, so a `listChanged` notification is
// sent because something happened rather than because a timer fired.
Wakeups Wakeups
Signals StreamSignals
// Clock is injectable so the tests do not have to wait. Nil means the system clock.
Clock func() time.Time
// Name and Version identify the server on initialize.
Name string
Version string
}
Server answers MCP over HTTP at /mcp (ai-first.md §1.1).
It is an adapter and nothing more: it translates JSON-RPC into a catalogue call and the result back. There is no authorisation here and no business rule - an agent reaching a use case through this door is checked by the same application layer as a person reaching it through REST, which is what makes an agent's action as safe, and as auditable, as anybody else's (ADR-0005, ADR-0012).
type Sessions ¶
type Sessions interface {
Issue(tenantID, accountID shared.ID, now time.Time) string
Validate(session string, tenantID, accountID shared.ID, now time.Time) error
}
Sessions is the slice of the session issuer this server needs (infrastructure/security).
An interface because presentation may not import infrastructure, and a narrow one because this is the whole of what the transport does with a session: hand one out, and check one against the actor presenting it.
type StreamSignals ¶
type StreamSignals interface {
StreamOpened(ctx context.Context)
StreamClosed(ctx context.Context, seconds float64)
StreamRefused(ctx context.Context, reason string)
}
StreamSignals is the slice of the metrics adapter this stream reports through - the same one the change stream uses, so an operator reads one set of series for both.
type Tool ¶
type Tool struct {
Name string `json:"name"`
// Description carries the preconditions and side effects an agent needs in order to decide
// whether to call the tool (ai-first.md §1.1). It is protocol documentation like an OpenAPI
// description, not display text - what a user sees is rendered from a message code.
Description string `json:"description"`
InputSchema map[string]any `json:"inputSchema"`
Annotations Annotations `json:"annotations"`
}
Tool is one entry of tools/list.
func ToolsOf ¶
func ToolsOf(descriptors []usecase.Descriptor) []Tool
ToolsOf renders the catalogue as MCP tools, in the catalogue's own order.
type Wakeups ¶
Wakeups tells the stream that a workspace has changed, so a notification is sent because something happened rather than because a timer fired (ADR-0007).
The same subscription the change stream uses. What differs is what it produces: a browser is told *what* changed, record by record; an agent is told only *that* its resource list may have moved, because MCP's `listChanged` carries no payload and an agent re-reads what it needs.