mcp server
mcp
Expose an Arandu application to an AI client, through the policies it already has.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 27, 2026What this is
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, speaking protocol
revision 2024-11-05, over HTTP or over stdio.
Install
go get github.com/arandu-io/mcp
A tool
// app/Mcp/ListPosts.go
type ListPosts struct{ svc *services.PostService }
func (ListPosts) Name() string { return "list_posts" }
func (ListPosts) Description() string { return "Lists the posts of this blog, newest first." }
func (ListPosts) Schema() mcp.Schema {
return mcp.Object(
mcp.String("status", "Which posts to list").Enum("published", "draft"),
mcp.Int("limit", "How many to return"),
)
}
func (t ListPosts) Handle(ctx context.Context, r mcp.Request) (mcp.Response, error) {
limit, _ := r.Int("limit")
// The subject the request carried, through the service, through the policy.
found, err := t.svc.List(ctx, r.Subject(), database.Query{Limit: limit})
if err != nil {
return mcp.Response{}, err
}
// The fields that may leave, listed once in a JSON Resource -- the same one
// a controller answers with.
return mcp.JSON(resources.NewPostList(found)), nil
}
A server, and where it is reachable
The tools live in app/Mcp/, the server is composed in bootstrap/app.go
like every other part of the application, and it is mounted in
routes/web.go, so the route table stays one table.
// bootstrap/app.go
server := &mcp.Server{
Name: "blog",
Version: "1.0.0",
Instructions: "The posts and comments of this blog. Drafts are not public.",
Tools: []mcp.Tool{ListPosts{svc}, PublishPost{svc}},
}
// routes/web.go -- over HTTP, for a remote client. The guard in front of the
// route puts the subject on the request and mcp.Web serves that subject:
// RequireToken for a client holding an API token, as here, or RequireAuth for
// one holding a session, on a route CSRFProtect guards.
r.Action("POST", "/mcp", mcp.Web(server), middleware.RequireToken(tokens)).Name("mcp")
// Over stdio, for an assistant on this machine. The application supplies the
// concrete server and the subject that its own policy has established.
mcp.Start(ctx, server, assistant)
The part that is not a port
The shape above is the one this ecosystem's users already know, deliberately. One thing is different, and it is the reason this package exists rather than a generic Go MCP library.
A tool reaches data the way a controller does. The Subject is on the
Request, the tool hands it to a service, the service asks the policy, and the
auth.Grant the policy issues is what the repository below requires. A policy
that refuses a tool refuses it for the same reason it refuses a controller —
there is no second enforcement point, and no way to write one by accident.
The server does not check that a tool did that. It validates the arguments, carries the subject and marks a failure as one; it runs no policy of its own, because a policy decides about a typed record and a tool declares none. So the boundary is the service, and that is not tidiness: an MCP server hands a language model the keys to an application, and a tool that queries the database directly is the largest hole an application could ship. It would ship quietly, because the answers would look right.
Where the subject comes from is the transport's answer, and the two are different on purpose:
mcp.Web | from the request, exactly like a controller reads it: the subject RequireToken or RequireAuth put there. A request that carries none is refused with 401, never served as a guest |
mcp.Local | from configuration, over a pipe. There is no session on stdio, so the identity is declared where the server is started, in code a reviewer reads |
The local one takes a Subject rather than defaulting to one, so an application
that lets an assistant act as an administrator has written that down somewhere a
reviewer reads.
Three smaller decisions
A refusal is an error, not an empty result. A model handed an empty list
concludes there is nothing there and tells somebody. A model told it may not,
stops. For a tool it is one boolean, isError, and it is the difference between
"you have no invoices" and "you cannot see them"; a resource that could not be
read and a prompt that could not be rendered are JSON-RPC errors, never an empty
document or an empty conversation.
An argument nobody declared is refused. A model that invents a parameter and is not told keeps inventing it — and a tool that reads only what it declared acts on a call it half understood.
A tool with no description is refused by Server.Validate. The description
is what the model reads to decide whether to call it: it is the
highest-leverage string in this package, and a tool without one is called at
random. It is a mistake in a declaration, so it belongs at boot rather than at
the first call. Over stdio that is automatic, because Local runs Validate
and refuses to serve; Web does not, so a server that is only mounted on a
route is checked where the application boots it.
What is deliberately absent
- No attributes, because Go has none. A tool's name and description are methods: more characters, one less mechanism.
- No facade, and no dynamic registration. A server declares its tools, resources and prompts in three slices, so one that exists and is not reachable is visible in one file.
- No sampling and no roots. They are in the protocol and nothing in an Arandu application needs them yet; a capability declared and not served is one a client reports as the server being broken.
Learning Arandu
The API reference is generated from the doc comments and lives on pkg.go.dev. Every exported symbol carries one, and that is deliberate: it is the documentation that cannot drift from the code, because it sits in the same file.
The CLI documents itself. aru help lists every command, and each one explains
what it writes and what to do with it. aru doctor explains what it found and
what breaks, not which rule was violated.
The guide is published at arandu.io/docs, and the site is itself an Arandu application. Where the guide and a doc comment disagree, the doc comment sits next to the code and is the one to trust.
Contributing
See CONTRIBUTING.md.
Security Vulnerabilities
Please review our security policy on how to report a vulnerability. Never open a public issue for one.
License
Open-sourced software licensed under the MIT license.