toolify

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 4 Imported by: 0

README

mcp-toolify

English | 简体中文

Go Reference MCP Stars

Turn a plain Go function into an MCP tool with one comment — // mcp:tool, go generate, done.

mcp-toolify is a code generator plus runtime for building Model Context Protocol servers in Go. Annotate an ordinary function's godoc with // mcp:tool, run go generate, and it becomes a fully-typed MCP tool — input struct, JSON schema, and registration all generated for you. No hand-written wrappers, no runtime reflection. The runtime adds the things real deployments need: connection-level authorization, automatic spill of oversized results, and call auditing.

Write the function, tag it, generate. Your tool's schema, description, argument docs, and risk metadata all come straight from the code you already wrote.

Built in Go, official MCP SDK, stdio + Streamable HTTP. Drop it into Cursor, Claude, Comate, or any MCP client — or embed it into a host server you already run.


Why mcp-toolify

Most ways to expose Go logic as MCP tools mean hand-writing a wrapper per function: an input struct, a JSON schema, argument descriptions, a handler that unpacks args and packs results, plus registration boilerplate. It drifts from the real function the moment you touch it, and it says nothing about risk, size, or auth. mcp-toolify nails five things:

  • 1. Annotation-driven, zero boilerplate — the code is the spec. Add // mcp:tool to a function's godoc and a standalone generator (cmd/mcpgen) emits a typed wrapper: input struct from the parameters, JSON-schema descriptions from param: lines, the tool description from the doc comment. Generated code calls your function directly — no runtime reflection, and the tool can never silently drift from the signature.
  • 2. Oversized results spill to disk — the model context never blows up. Every tool return is measured; anything over a configurable token budget is written to a temp file and returned as an MCP resource link + short summary instead of a giant payload. A built-in companion tool, spill_explore (read / grep / schema / jq over json/jsonl/text), lets the agent explore the blob on demand and pull only the lines it needs. Cross-machine deployments get a direct download URL for free, and in a multi-replica setup spill_explore still works no matter which replica the call lands on — it rides the stateful-tool routing below (the built-in, canonical example of that feature).
  • 3. Stateful tools across replicas — resources live on one replica, calls land anywhere. Some tools produce a resource that only exists on the replica that created it (a spill blob, a live session, a long-running job, a probe handle). In a load-balanced deployment a follow-up call (read it, poll it, cancel it) can land on a different replica and miss. mcp-toolify solves this generically: encode the owning replica into the resource id, declare which argument carries that id with RegisterOwnerRouted(tool, param), and a call-level middleware (WithOwnerRouting) transparently reverse-proxies the whole tools/call to the owning replica — re-authorized there, gated by a sibling allow-list, single-hop. Your tool code stays a plain local function; the framework handles the distribution.
  • 4. Connection-level authorization, two layers. Per-token read/write risk ceilings decide what a connection may ever do (and filter tools/list accordingly); tools/call additionally checks the caller identity against a per-risk allow-list. One agent can share a connection across many users, each still gated by who they are. Tools declare risk with mcp:risk=low|medium|high and write-intent with the write tag.
  • 5. Standalone or embedded — share one server. Run it over stdio or HTTP as its own process, or get two http.Handlers and mount onto an HTTP server you already have, sharing the port and lifecycle. External, hand-written tools can be registered onto the same server alongside the generated ones (just register their risk metadata).
  • 6. No proprietary dependencies — clean, portable, auditable. Only the official Go MCP SDK, jsonschema-go, BurntSushi/toml, and (for the built-in spill tool) gojq. The code generator additionally uses golang.org/x/tools + yaml.v3, pulled in only when you run codegen. Nothing else to trust.

Plus the machinery that makes the above reliable:

  • Honest schemas for tricky types. interface{} parameters get an explicit half-restricted JSON schema (a type union) instead of the SDK's unconstrained empty node, so the model still gets type hints. Multiple return values are packed into a stable, named JSON object.
  • Interface parameters, handled. A parameter the model can't build from JSON (an interface) is bound to a concrete type with mcp:bind=param:Type; mcp:import=<path> pulls in an external package for that type. Fully generic — no framework-specific special-casing.
  • Pluggable auditing. Inject a Logger to record every call (user, tool, args, result, cost) as structured fields; with none set it falls back to the standard library.
  • Bounded, self-cleaning spill store. Spill files live under a temp dir with per-category TTLs and a background GC; nothing accumulates forever.

Quick start

Requires Go 1.25+.

1. Annotate a function.

package greeter

// Greet builds a greeting.
//
// param: name — the name to greet
// param: excited — add an exclamation mark
//
// mcp:tool
// mcp:tags=read
func Greet(name string, excited bool) (string, error) {
	if name == "" {
		return "", fmt.Errorf("name is required")
	}
	msg := "Hello, " + name
	if excited {
		msg += "!"
	}
	return msg, nil
}

2. Create mcpgen.yaml in your own module — list the packages to scan and where to write the generated wrappers. Paths are relative to this file's directory.

output:
  dir: ./tools
packages:
  - github.com/you/yourmod/greeter
  # optional: expose the built-in large-result explorer
  - github.com/fzxbl/mcp-toolify/spillexplore

3. Add a //go:generate directive in a Go file next to that mcpgen.yaml (e.g. gen.go), then run it. -config resolves relative to that file's directory.

//go:generate go run github.com/fzxbl/mcp-toolify/cmd/mcpgen -config ./mcpgen.yaml
go generate ./...

4. Start a server.

package main

import (
	"context"

	toolify "github.com/fzxbl/mcp-toolify"
	"github.com/you/yourmod/tools" // generated
)

func main() {
	_ = toolify.Start(context.Background(),
		toolify.Config{Transport: "stdio"}, tools.RegisterAll)
}

A runnable end-to-end sample lives in example/. Inspect the exposed tools (name + description + schema) with:

go run ./cmd/listtools -short

Annotation markers

All live in the function's godoc comment:

  • mcp:tool — expose this function (required).
  • mcp:name=<n> — override the tool name (default <pkg>.<snake_case_func>).
  • mcp:tags=a,b — tags for start-up filtering; the write tag marks a write operation for authz.
  • mcp:risk=low|medium|high — risk level (default none), enforced by HTTP authz.
  • mcp:bind=<param>:<Type> — bind an interface parameter (not JSON-constructible) to a concrete input type.
  • mcp:import=<path> — import path for a package referenced by a mcp:bind type outside the source package.

param: <name> — <desc> lines become per-argument JSON-schema descriptions.

Run over HTTP / mount onto an existing server

toolify.Start with Config{Transport: "http", Addr: ":8080"} runs a standalone HTTP server. To mount onto a server you already have:

mcpH, spillH, err := toolify.Handlers(cfg, tools.RegisterAll)
mux.Handle("/mcp", mcpH)      // MCP endpoint (Streamable HTTP)
mux.Handle("/spill/", spillH) // large-result download endpoint

Authorization

Set Config.AuthzEnabled = true (HTTP only) and point Config.ConfigPath at a TOML file:

identity_headers = ["X-MCP-User"]   # ordered headers for caller identity; first non-empty wins (top-level; omit => ["X-MCP-User"])

[spill]
max_result_tokens = 4000   # results above this estimate spill to disk; -1 disables
# Optional stateful-tool routing across replicas (off unless a sibling allow-list is provided):
# peer_timeout_ms = 5000                # proxy call timeout; 0 => 5000
# peer_hosts      = ["replica-a:8011"]  # static allow-list of sibling replicas that may be proxied to

[[tokens]]
token = "..."          # Authorization: Bearer <token>
name = "readonly-agent"
applicant = "you"
read = "medium"        # max read risk; omit to disallow reads
# write = "low"        # max write risk; omit to disallow writes

[risk_allowlist]
high = ["alice"]       # identity values allowed to run high-risk tools
medium = ["bob"]
  • Connection + tools/list are filtered by the token's ceilings only (what this connection can ever do).
  • tools/call additionally checks the caller identity (resolved from identity_headers front-to-back, first non-empty wins; default X-MCP-User) against risk_allowlist for medium/high tools.

Audit headers

Point Config.ConfigPath at the same TOML file to record chosen request headers into the audit log. This is audit-only and independent of AuthzEnabled:

[audit]
headers = ["X-Tenant-Id", "X-Client-Id"]

Each listed header becomes its own audit field, keyed by the lowercased header name (e.g. X-Tenant-Idx-tenant-id); a missing header is logged as -. Omit the section (or leave it empty) to record no extra headers. Values are trusted as-is, so only enable this behind a trusted gateway and avoid listing credential headers (Authorization, Cookie).

Request id & access log

Every HTTP request carries a logid for cross-log correlation:

[log]
logid_header = "X-Log-Id"   # header to read the incoming logid from; omit => "X-Log-Id"

HTTPLogID takes the logid from that header (or generates one when absent), injects it into the request context, echoes it back in the same response header, and records it as the logid field of every audit record. When you run the built-in standalone server (Start/Run over HTTP) it also emits a per-request access log (logid, method, path, status, cost, user) through the same Logger, so a request can be traced across the access log and the audit log by its logid. When you mount the handlers into your own server (Handlers), no access log is added — the logid is still injected and echoed so your host's access log can correlate.

Stateful tools across replicas

Most tools are stateless — any replica can serve any call. But some tools produce a resource that physically lives on the replica that created it: a spilled result on local disk, an interactive session, a long-running job or probe. In a load-balanced deployment a follow-up call (read the blob, poll the job, cancel the probe) can land on a different replica and miss. mcp-toolify makes such tools stateful-aware with an owner-encoded id plus a call-level routing middleware — no shared storage, no logic registration, your tool stays a plain local function:

  • Encode the owner into the id. When a tool mints an id for a resource it owns, it embeds its own address (derived from Config.PublicBaseURL, the same field that produces the direct download URL). Without a base URL, ids stay in the legacy random form and no routing happens.

  • Declare which argument carries the id. Register the tool's routing parameter once, at init time:

    func init() {
        // follow-up calls that take an owned id route to its owner replica
        runtime.RegisterOwnerRouted("your.get_status", "job_id")
        runtime.RegisterOwnerRouted("your.cancel",     "job_id")
    }
    
  • The middleware does the rest. WithOwnerRouting inspects each incoming tools/call: if the declared argument holds an id owned by a remote sibling on the allow-list, it reverse-proxies the whole call to that owner's /mcp and streams the result back; otherwise it passes through to the local handler. Forwarding is capped to a single hop by a loop-guard header. The forwarded request is re-authorized on the owner replica, so routing grants no extra privilege.

The built-in spill_explore tool is the canonical example — it's registered exactly this way, which is why exploring a spilled result works no matter which replica the call lands on.

Wire it up once, then let every stateful tool ride it:

mux.Handle("/mcp", toolify.WithOwnerRouting(mcpH)) // wrap the MCP handler

Forward targets are restricted to a live sibling-replica allow-list. Provide it statically via peer_hosts, or wire your own service discovery:

toolify.SetSpillPeerProvider(func() []string { return currentReplicaHostPorts() })
// or a static snapshot:
toolify.SetSpillPeers([]string{"replica-a:8011", "replica-b:8011"})

An empty list (and no provider) denies all remote forwarding, preventing SSRF — a call for a remote owner that isn't on the list is served locally (and simply misses) rather than proxied anywhere.

Layout

  • toolify.go — public entry points: Start, Handlers, Config, Logger, SetAuditLogger, RegisterToolMeta, WithOwnerRouting, RegisterOwnerRouted, OwnerOf.
  • runtime/ — server wiring, authz, spill store, owner routing, audit logging.
  • spillexplore/ — the built-in spill_explore tool.
  • cmd/mcpgen/ — the code generator (go run github.com/fzxbl/mcp-toolify/cmd/mcpgen).
  • cmd/listtools/ — dev helper to dump exposed tools + schemas.
  • example/ — a minimal, runnable end-to-end sample.

Generated *_gen.go files are normally not checked in (regenerated on build); this repo commits example/tools/ only so the sample builds out of the box.

Security

When served over HTTP without AuthzEnabled, the MCP endpoint is unauthenticated — calling a tool runs your Go code. Bind to loopback or front it with an authenticating proxy, or enable authz. Note that X-MCP-User is trusted as-is, so it must be injected by a trusted gateway, never accepted from clients directly.

License

MIT — see LICENSE. Contributions and stars welcome.

Documentation

Overview

Package toolify 把带 `// mcp:tool` 标记的 Go 函数暴露为 MCP(Model Context Protocol)工具。生成器(cmd/mcpgen)扫描注解生成 wrapper 代码,本包提供把这些 wrapper 挂到 MCP server 上运行的入口(stdio / http),并附带连接级鉴权、 大返回结果落盘(spill)与调用审计等运行时能力。

典型用法:

//go:generate go run github.com/fzxbl/mcp-toolify/cmd/mcpgen -config ./mcpgen.yaml
func main() {
    _ = toolify.Start(context.Background(), toolify.Config{Transport: "stdio"}, tools.RegisterAll)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Handlers

func Handlers(cfg Config, registrar Registrar, extra ...func(*mcp.Server)) (mcpHandler, spillHandler http.Handler, err error)

Handlers 构造用于挂载到既有 HTTP server 的两个 http.Handler:

  • mcpHandler:MCP 协议端点(Streamable HTTP),可挂在任意子路径(如 /mcp)。
  • spillHandler:大返回结果的临时下载端点(默认 /spill/<id>)。

与 Start 不同,Handlers 不自己监听端口,而是把 handler 交给调用方挂载到已有的 HTTP server 上,从而与主服务共用同一端口与生命周期。

extra 为可选的额外工具注册器:除生成的 registrar 外,把外部模块的工具注册到 同一个 server。注意:外部工具需另行调用 RegisterToolMeta 登记风险等级, 否则在 authz 中按“未登记默认放行”处理。

若 cfg.PublicBaseURL 非空,会设置 spill 下载端点的对外基础地址(跨机部署时 agent 直连下载用)。

func OwnerOf added in v0.4.0

func OwnerOf(id string) (string, bool)

OwnerOf 解出 owned id 的属主 host:port;ok=false 表示旧式/无归属 id。

func RegisterOwnerRouted added in v0.4.0

func RegisterOwnerRouted(toolName, paramName string)

RegisterOwnerRouted 声明「工具按某 owned-id 参数路由」,供有状态工具接入分布式层。

func RegisterToolMeta

func RegisterToolMeta(name string, write bool, risk string)

RegisterToolMeta 为“非注解生成、运行时注册”的外部工具登记鉴权元数据(能力+风险)。 write=true 表示写操作(ReadWrite),否则只读;risk 取 none|low|medium|high。 必须在处理请求前调用,否则该工具在 authz 里按“未登记默认放行”处理。

func SetAuditLogger

func SetAuditLogger(l Logger)

SetAuditLogger 注入调用审计 logger。应在启动/挂载 server 前调用。

func SetSpillPeerProvider added in v0.2.0

func SetSpillPeerProvider(fn func() []string)

SetSpillPeerProvider 注册 spill 跨副本代理的动态兄弟副本发现函数(返回可达 host:port 列表)。 多副本部署时用它对接任意服务发现作为代理转发白名单来源,无需静态配置;传 nil 清除。 与 SetSpillPeers 互为覆盖,后调用者生效。应在挂载/启动 server 前调用。

func SetSpillPeers added in v0.2.0

func SetSpillPeers(hosts []string)

SetSpillPeers 设置静态兄弟副本白名单(host:port);简单部署可用它替代 provider。 与 SetSpillPeerProvider 互为覆盖,后调用者生效。

func Start

func Start(ctx context.Context, cfg Config, registrar Registrar) error

Start 用给定 registrar 启动 MCP server,阻塞直到 ctx 取消或 server 退出。

func WithOwnerRouting added in v0.4.0

func WithOwnerRouting(next http.Handler) http.Handler

WithOwnerRouting 包裹 MCP handler:把归属兄弟副本的 tools/call 反代到属主 /mcp。 挂载到既有 HTTP server 时套在 MCP handler 外层。

Types

type Config

type Config = runtime.Config

Config 是 server 启动配置(runtime.Config 的别名,外部只需 import 本包)。

type Logger

type Logger = runtime.Logger

Logger 是 MCP 调用审计日志接口;不注入时回退到标准 log。

type RegisterOptions

type RegisterOptions = runtime.RegisterOptions

RegisterOptions 控制启用哪些生成的工具(按包名/标签过滤)。

type Registrar

type Registrar = runtime.Registrar

Registrar 是生成代码暴露的注册函数类型(通常是生成的 tools.RegisterAll)。

Directories

Path Synopsis
cmd
listtools command
listtools 用 in-memory transport 启动 mcp server,验证暴露给 MCP 客户端 (Cherry Studio / Claude Desktop 等)的完整元数据。
listtools 用 in-memory transport 启动 mcp server,验证暴露给 MCP 客户端 (Cherry Studio / Claude Desktop 等)的完整元数据。
mcpgen command
Package example wires code generation for the runnable sample: `go generate` reads mcpgen.yaml and writes the typed tool wrappers into ./tools.
Package example wires code generation for the runnable sample: `go generate` reads mcpgen.yaml and writes the typed tool wrappers into ./tools.
cmd/server command
Command server is a minimal mcp-toolify server exposing the example tools.
Command server is a minimal mcp-toolify server exposing the example tools.
greeter
Package greeter is a tiny example of tools exposed via mcp-toolify.
Package greeter is a tiny example of tools exposed via mcp-toolify.
Package runtime provides the hand-written runtime substrate for the auto-generated MCP tool registrations under mcp/tools.
Package runtime provides the hand-written runtime substrate for the auto-generated MCP tool registrations under mcp/tools.

Jump to

Keyboard shortcuts

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