Documentation
¶
Overview ¶
Package mcp exposes an Arandu application to an AI client.
The Model Context Protocol is how an assistant reaches a program: the program declares tools it can call, resources it can read and prompts it can use, and the client picks. This package is the Arandu side of that.
Every tool carries a Grant ¶
A tool reaches data, and every path to data in this framework carries a security.Grant. So does this one.
func (t Invoices) Handle(ctx context.Context, r mcp.Request) (mcp.Response, error) {
found, err := t.svc.List(ctx, r.Subject(), data.Query{Limit: 20})
…
}
The Subject is on the Request and there is no way to call a service without one. That is not politeness -- an MCP server is a program that hands a language model the keys to an application, and the version of this package where a tool queries the database directly would be the largest hole this project could ship. A policy that refuses a tool refuses it for the same reason it refuses a controller.
Where the Subject comes from is the transport's answer, and the two are deliberately different:
Web from the session, exactly like an HTTP request. A remote client is
somebody signed in, or it is nobody.
Local from configuration, over stdio. There is no session on a pipe, so the
identity is declared when the server is registered and is visible in
routes/ai.go rather than assumed.
What is deliberately absent ¶
No attributes, because Go has none: a tool's name and description are methods, which is more characters and one less mechanism. No facade. No dynamic registration -- a server declares its tools in a slice, so a tool that exists and is not reachable is visible in one file.
Index ¶
- Constants
- func Describe(s *Server, out io.Writer)
- func Local(ctx context.Context, s *Server, subject security.Subject, in io.Reader, ...) error
- func Start(ctx context.Context, s *Server, subject security.Subject) error
- func Web(s *Server, sessions *security.SessionStore, tenant string) func(*fhttp.Context) error
- type Argument
- type Field
- type Message
- type Prompt
- type Request
- type Resource
- type Response
- type Schema
- type Server
- func (s *Server) Call(ctx context.Context, subject security.Subject, name string, ...) Response
- func (s *Server) Handle(ctx context.Context, subject security.Subject, body []byte) []byte
- func (s *Server) Read(ctx context.Context, subject security.Subject, uri string) Response
- func (s *Server) Tool(name string) (Tool, bool)
- func (s *Server) Validate() error
- type Tool
Constants ¶
const MaxMessage = 1 << 20
MaxMessage is the largest message either transport reads, in bytes.
A message is held whole before it can be parsed, so without a bound the process holds whatever the other end sends -- and sending is the cheap half of that exchange. The number is the same on both transports: a message one accepts and the other refuses is a message whose fate depends on how it arrived, which is the hardest kind of report to act on.
const Version = "2024-11-05"
Version is the protocol revision this package speaks.
Variables ¶
This section is empty.
Functions ¶
func Describe ¶
Describe prints what a server offers, for `aru mcp:list`.
It exists because the alternative is connecting a client to find out, and the question "what can this thing do" is asked far more often than it is answered by an assistant.
func Local ¶
func Local(ctx context.Context, s *Server, subject security.Subject, in io.Reader, out io.Writer) error
Local serves the server over stdin and stdout.
It is what a client on the same machine starts, and it is `aru mcp:start`. One message per line, which is how the protocol frames itself on a pipe, and no message longer than MaxMessage.
Nothing is ever written to stdout except an answer. A log line there is a parse error at the client, and it is the most common way a stdio server appears broken while working -- so the logger is the framework's, which writes to stderr.
Types ¶
type Field ¶
type Field struct {
// contains filtered or unexported fields
}
Field is one declared argument.
type Prompt ¶
type Prompt interface {
Name() string
Description() string
// Arguments are what the client fills in before the prompt is useful.
Arguments() []Argument
// Render builds the messages.
Render(ctx context.Context, r Request) ([]Message, error)
}
Prompt is a conversation an application knows how to start.
type Request ¶
type Request struct {
// Arguments are what the client sent, already checked against the tool's
// schema. Read them with String, Int and Bool rather than by indexing: a
// missing key is a zero value, and a tool that cannot tell "0" from "absent"
// is a tool that acts on an argument nobody passed.
Arguments map[string]any
// contains filtered or unexported fields
}
Request is one call from a client.
func (Request) Int ¶
Int reads a number. JSON has one numeric type and it decodes as float64, so this is where that stops being the caller's problem.
type Resource ¶
type Resource interface {
// URI addresses it, in a scheme of the application's choosing.
URI() string
// Name and Description are what the client lists it as.
Name() string
Description() string
// MimeType is what the content is. Empty means text/plain.
MimeType() string
// Read returns the content.
Read(ctx context.Context, s security.Subject) (Response, error)
}
Resource is something a client can read.
type Response ¶
type Response struct {
// Text is what the client shows or feeds to the model.
Text string
// IsError marks the answer as a failure the model should react to rather
// than a result. A refused authorization is one of these: the model should
// learn it may not, not be handed an empty list and conclude there is
// nothing there.
IsError bool
}
Response is what a tool answers with.
type Schema ¶
type Schema struct {
// contains filtered or unexported fields
}
Schema declares what a tool takes.
It is a small typed builder rather than a map or a struct tag, for the reason the rest of this framework prefers a signature to a convention: a schema written as JSON in a string is a schema nothing checks, and the first time it is wrong the model sends an argument the tool ignores.
mcp.Object(
mcp.String("slug", "The post to read").Required(),
mcp.Int("limit", "How many to return"),
)
func (Schema) Validate ¶
Validate checks a call's arguments against the schema.
It runs before Handle, so a tool never sees an argument it did not declare or a missing one it marked required -- which is what lets a tool read an argument without checking, and what stops a model's invented parameter from reaching application code.
Every problem is reported at once. A model that is told one mistake per call spends three calls on a form it could have filled in on the second.
type Server ¶
type Server struct {
// Name and Version identify the server to the client.
Name string
Version string
// Instructions are read by the model before anything else, and they are the
// place to say what this application is for. A server whose instructions are
// empty is a server the model guesses about.
Instructions string
Tools []Tool
Resources []Resource
Prompts []Prompt
}
Server is what a client connects to: a name, and what it can do.
The three lists are slices rather than a registry somebody appends to at boot. A tool that exists and is not reachable is then visible in one file, which is the same reason bootstrap/app.go is a list rather than a container.
func (*Server) Call ¶
func (s *Server) Call(ctx context.Context, subject security.Subject, name string, args map[string]any) Response
Call runs a tool, as the given subject.
This is the one door. Both transports come through it, so the validation, the authorization boundary and the shape of a failure are decided once -- and a third transport cannot arrive with its own idea of any of them.
func (*Server) Handle ¶
Handle answers one JSON-RPC message.
It returns nil for a notification -- a message with no id, which the protocol says gets no answer. Sending one anyway is what makes a client hang up.
func (*Server) Validate ¶
Validate reports what is wrong with the server itself.
It runs at boot rather than at the first call, because everything it checks is a mistake in a declaration: two tools with one name, a tool with no description, a schema field nobody named. A server that starts and answers nonsense is worse than one that refuses to start.
type Tool ¶
type Tool interface {
// Name is what the client calls it by. Lower case with underscores, because
// that is what every client displays without quoting.
Name() string
// Description is what the model reads to decide whether to call it. It is
// the single highest-leverage string in this package: a model that calls the
// wrong tool was told the wrong thing here.
Description() string
// Schema declares the arguments. A call whose arguments do not match is
// refused before Handle runs.
Schema() Schema
// Handle does the work. The Grant comes from the Request's Subject, through
// the service, through the policy -- like everywhere else.
Handle(ctx context.Context, r Request) (Response, error)
}
Tool is something a client can call.