mcp

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

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

View Source
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.

View Source
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).

View Source
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.

View Source
const Scheme = "hubtask://"

Scheme is the URI scheme this server addresses its content under (ai-first.md §1.1).

View Source
const SessionHeader = "Mcp-Session-Id"

SessionHeader is where MCP carries the session identifier, in both directions.

Variables

This section is empty.

Functions

func InputSchema

func InputSchema(fields []usecase.Field) map[string]any

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

func PromptMessages(prompt port.Prompt, arguments map[string]string) ([]map[string]any, error)

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

func PromptName(prompt port.Prompt) string

PromptName is how a prompt is addressed: `<id>@<version>`.

func URIOf

func URIOf(segment string, id shared.ID) string

URIOf builds the address of one instance.

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.

func (ErrURI) Error

func (e ErrURI) Error() string

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).

func (Server) ServeHTTP

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

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

type Wakeups interface {
	Subscribe(tenantID shared.ID) (<-chan struct{}, func())
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL