Skip to content
Conduit

mcp server

mcp

Expose an Arandu application to an AI client, through the policies it already has.

arandu-io/mcpcommunitylistedunreviewed

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, 2026

What 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.Webfrom 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.Localfrom 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.

↑↓ navigate openesc close