toolify

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 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: the producing replica's address is encoded in the spill id, so a local miss single-hops to that owner replica's internal endpoint and returns only the small explored result (opt-in, secret-guarded, allow-listed).
  • 3. 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.
  • 4. 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).
  • 5. 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 generator is its own module with just golang.org/x/tools + yaml.v3. 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. List your packages in mcpgen.yaml.

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

3. Generate the wrappers.

//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 multi-replica proxy (off unless peer_token is set):
# peer_token     = "..."                # shared secret for the internal /spill-explore endpoint; empty => proxy off
# peer_timeout_ms = 5000                # proxy call timeout; 0 => 5000
# peer_hosts     = ["replica-a:8011"]   # static allowlist 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.

Multi-replica spill

By default a spill resource lives only on the disk of the replica that produced it, so in a load-balanced deployment a later spill_explore can land on a different replica and miss. mcp-toolify solves this with an owner-encoded id plus a single-hop proxy — no shared storage required:

  • The producing replica's own address is encoded into the spill id, 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 proxying happens.
  • On a local miss, if the id names a remote owner that is on the allow-list, the receiving replica POSTs the explore request to the owner's internal /spill-explore endpoint and returns only the small explored result. The endpoint calls the local explorer only, so forwarding is structurally limited to a single hop.

Security is opt-in and layered:

  • The endpoint is guarded by a shared secret (peer_token, constant-time compared). An empty secret disables the whole proxy and makes the endpoint return 404.

  • 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 and secret leakage. Request and response bodies are size-capped.

When you mount onto your own server, expose the endpoint with mux.Handle("/spill-explore", toolify.SpillExploreEndpoint()).

Layout

  • toolify.go — public entry points: Start, Handlers, Config, Logger, SetAuditLogger, RegisterToolMeta.
  • runtime/ — server wiring, authz, spill store, audit logging.
  • spillexplore/ — the built-in spill_explore tool.
  • cmd/mcpgen/ — the code generator (its own module).
  • 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 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 SpillExploreEndpoint added in v0.2.0

func SpillExploreEndpoint() http.Handler

SpillExploreEndpoint 返回副本间内部 explore 端点(POST,共享密钥鉴权)。用 Handlers 挂载到 既有 HTTP server 时,把它挂在 "/spill-explore" 路径即可让多副本 spill_explore 正常工作; 用 Start 独立启动时该端点已自动注册。共享密钥([spill].peer_token)未配置时端点返回 404。

func Start

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

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

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
example
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