mcp

package
v0.11.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package mcp is the bridge between weft and the Model Context Protocol, both directions over the official Go SDK (github.com/modelcontextprotocol/go-sdk, aliased `sdk` in examples — this package keeps the name `mcp`):

tools, err := mcp.Tools(ctx, sess, mcp.Prefix("gh_"))
var skipped *mcp.ImportError
if errors.As(err, &skipped) { // the good tools imported; these did not
	slog.Warn("mcp: tools skipped", "err", skipped)
	err = nil
}
mcp.AddTools(srv, lookup, refund)
mcp.Serve(srv, agt, "Support agent.")

Consuming (§7.3): the tools of a connected client session become ordinary weft tools — a RawTool per server tool, the server's schema bytes verbatim (core.ParseSchema keeps every keyword the core's Schema type cannot express). Exposing (§7.2): weft tools and whole agents register on an SDK server; every failure — undecodable arguments, a handler error, a panic — is a tool result with isError, carrying the same text weft's own model would see (ADR 0002's pinned bytes, over the wire).

The bridge is mechanical by design (ADR 0003's shape bet, measured by §7.1's corpus): field-for-field maps between two already-compatible shapes, no translation layer of its own. This module is a satellite (ADR 0005): it imports the core, the core's internal/adapterkit, and the SDK — nothing else — and the core never imports it or names it.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrToolError = errors.New("mcp: tool returned an error")

ErrToolError is the cause on a remote isError result — data for the model, a sentinel for middleware.

Functions

func AddTools

func AddTools(s *sdk.Server, tools ...*core.ToolDef)

AddTools registers weft tools on an MCP server. Each tool is listed under its own name, description, input schema and — for a non-string output — output schema, and a call runs ToolDef.Invoke: the result text is returned as one text content item (and, when the tool has an output schema, as structuredContent holding the same JSON), and every failure — undecodable arguments, a handler error, a panic — is a tool result with isError, carrying the same text weft's loop would show its own model (ADR 0002's pinned bytes, over the wire). Run policy (Timeout, MaxResultBytes, WrapTools) is not applied: the exposition is the tool, not an agent; use Serve to export an agent's tools under its chain.

AddTools panics on a nil tool, a duplicate name within one call (the SDK replaces same-named tools silently and exposes no lookup, so a name already registered by an earlier call is replaced, not refused), or a tool built with RequireApproval — MCP has no approval channel, and running a gated tool unapproved would be a silent policy bypass.

Example

AddTools registers weft tools on an MCP server: the listing and the calls run the tool's own contract — no adapter layer, the tool is the exposition.

package main

import (
	"context"
	"fmt"

	sdk "github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/weftgo/weft"
	"github.com/weftgo/weft/mcp"
)

func main() {
	echo := weft.Tool("echo", "Echo the text.",
		func(_ context.Context, in struct {
			Text string `json:"text"`
		}) (string, error) {
			return "heard: " + in.Text, nil
		})
	srv := sdk.NewServer(&sdk.Implementation{Name: "demo", Version: "0"}, nil)
	mcp.AddTools(srv, echo)

	serverTransport, clientTransport := sdk.NewInMemoryTransports()
	go func() { _ = srv.Run(context.Background(), serverTransport) }()
	client := sdk.NewClient(&sdk.Implementation{Name: "demo-client", Version: "0"}, nil)
	sess, err := client.Connect(context.Background(), clientTransport, nil)
	if err != nil {
		panic(err)
	}
	res, err := sess.CallTool(context.Background(), &sdk.CallToolParams{
		Name:      "echo",
		Arguments: map[string]any{"text": "hello"},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(res.Content[0].(*sdk.TextContent).Text)
}
Output:
heard: hello

func Serve

func Serve(s *sdk.Server, a *core.Agent, description string)

Serve exposes an agent over MCP: one tool named after the agent (core.Name is required) that runs it on a prompt and returns its final text or submitted output, plus the agent's tools, each dispatched through the agent's tool chain (Agent.CallTool) so middleware and approval policy apply — an approval-gated tool answers with the ErrApprovalRequired text rather than running, loud where AddTools refuses outright. Run policy (Timeout, MaxResultBytes) is not applied, per CallTool's rule; the SDK's own request handling bounds the call.

The agent tool is core.Subagent under the name: the same {"prompt": string} input schema, the same Output handling, the same final-text rule — Serve is Subagent over the wire. description is what MCP clients (and their models) read to decide when to call the agent: it is the whole routing surface, as for Subagent, and it is written by the caller, never synthesised.

The tools exposed are the agent's registered ones (Agent.Tools); a ToolSource's snapshot is a per-step, runtime list and is not enumerated — its tools still run inside the agent tool's own steps.

Serve panics on a nil agent, an unnamed agent (the manifest's rule), or a name collision between the agent and one of its tools.

Example

Serve exposes a whole agent as one MCP tool — named after the agent, described for routing — plus the agent's tools under its chain.

package main

import (
	"context"
	"fmt"

	sdk "github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/weftgo/weft"
	"github.com/weftgo/weft/mcp"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	agt := weft.New(wefttest.Script(wefttest.Say("research complete")),
		weft.Name("research"),
		weft.Instructions("You research."),
	)
	srv := sdk.NewServer(&sdk.Implementation{Name: "demo", Version: "0"}, nil)
	mcp.Serve(srv, agt, "Research a topic in depth.")

	serverTransport, clientTransport := sdk.NewInMemoryTransports()
	go func() { _ = srv.Run(context.Background(), serverTransport) }()
	client := sdk.NewClient(&sdk.Implementation{Name: "demo-client", Version: "0"}, nil)
	sess, err := client.Connect(context.Background(), clientTransport, nil)
	if err != nil {
		panic(err)
	}
	res, err := sess.CallTool(context.Background(), &sdk.CallToolParams{
		Name:      "research",
		Arguments: map[string]any{"prompt": "the history of denim"},
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(res.Content[0].(*sdk.TextContent).Text)
}
Output:
research complete

func Tools

func Tools(ctx context.Context, sess *sdk.ClientSession, opts ...Option) ([]*core.ToolDef, error)

Tools lists the tools of a connected MCP session and returns each as a weft tool: the server's name and description, its input schema as the SDK received it — every keyword preserved, an enum or a oneOf the core cannot express reaching the model whole (the SDK's client hands over decoded JSON, so the kept bytes are the document in encoding/json's canonical key order) — and a handler that forwards the model's argument bytes to tools/call. A remote result is returned as text (structuredContent's JSON when the server sends it, else the text items joined by newlines; non-text items render as one-line markers, because weft's transcript carries no binary tool results); a remote isError result is a tool error carrying the server's text verbatim (errors.Is ErrToolError; an empty one reads ErrToolError's own text); a transport failure is a tool error reading "mcp: <err>". Both are data the model sees, never run errors.

Imported tools are ordinary weft tools: register them with New or serve them through ToolSource; Timeout, MaxResultBytes, RequireApproval and WrapTools apply through Policy. A tool the server does not mark readOnlyHint is Sequential — foreign tools run one at a time unless the server says parallel is safe (annotations are untrusted hints; the conservative reading is the safe one). Progress notifications are not surfaced: the delegating call's ToolStart and ToolFinish bracket the remote call. Elicitation is not answered: a server that asks fails the call, as a tool error.

The SDK's client decodes the listed schema before Tools sees it, so the kept bytes are the document re-encoded: keys in canonical order, and an integer beyond 2^53 (a maximum on an id field, say) rounded through float64 — the one loss the bridge cannot avoid short of the SDK keeping raw bytes.

Tools returns when every page is listed or ctx ends; it returns the ctx error unchanged when the deadline hits mid-list. Connect several servers concurrently with an errgroup and one deadline; for a list that changes, set the SDK's ToolListChangedHandler to re-run Tools into a slice you serve through core.ToolSource, under a mutex (the SDK runs the handler on its own goroutine, the loop reads the source on the run's; ExampleTools_toolSource shows the shape) — Tools does not own the session and never closes it. The kill switch (WEFT_MODEL_REQUESTS) governs model requests; tools/call is not one, so imported tools run under deny — they are the caller's tools, like any handler that makes an HTTP call.

A tool whose output schema the server sends is recorded on the ToolDef when it is an object schema; a non-object output schema (legal per the spec) has no weft representation and is not recorded — the result text is the tool's output either way.

A tool that cannot be imported — an input schema the core cannot key on (a non-object root, a $ref root, an omitted schema), an empty name, a name repeated in the listing (the first importable occurrence stands: an earlier entry that was itself skipped claims nothing), a nil entry — fails that tool, not the listing: the importable tools are returned and the error is an *ImportError naming each skipped tool and why (ADR 0015's 2026-09-27 amendment). The slice is usable whether or not err is nil; the caller decides whether a skipped tool is a warning to log or a reason to stop:

tools, err := mcp.Tools(ctx, sess)
var skipped *mcp.ImportError
if errors.As(err, &skipped) {
	slog.Warn("mcp: tools skipped", "err", skipped)
	err = nil
}
if err != nil { // a listing failure: transport, ctx
	return err
}

Nothing is dropped silently — a server with one broken tool no longer takes its ninety-nine good ones down, and the one is still named. A listing failure (transport, the ctx ending) is the ordinary error with no tools.

Example

Tools imports a server's tools as ordinary weft tools: the server's schema bytes kept whole, its annotations mapped to per-tool policy, and the calls forwarded verbatim.

package main

import (
	"context"
	"encoding/json"
	"fmt"

	sdk "github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/weftgo/weft/mcp"
)

func main() {
	// Stand up a remote server standing in for a real one.
	srv := sdk.NewServer(&sdk.Implementation{Name: "remote", Version: "0"}, nil)
	srv.AddTool(&sdk.Tool{
		Name:        "search",
		Description: "Search the archive.",
		InputSchema: json.RawMessage(`{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}`),
		Annotations: &sdk.ToolAnnotations{ReadOnlyHint: true},
	}, func(_ context.Context, _ *sdk.CallToolRequest) (*sdk.CallToolResult, error) {
		return &sdk.CallToolResult{Content: []sdk.Content{&sdk.TextContent{Text: "3 hits"}}}, nil
	})
	serverTransport, clientTransport := sdk.NewInMemoryTransports()
	go func() { _ = srv.Run(context.Background(), serverTransport) }()
	client := sdk.NewClient(&sdk.Implementation{Name: "demo", Version: "0"}, nil)
	sess, err := client.Connect(context.Background(), clientTransport, nil)
	if err != nil {
		panic(err)
	}

	tools, err := mcp.Tools(context.Background(), sess, mcp.Prefix("arch_"))
	if err != nil {
		panic(err)
	}
	out, err := tools[0].Invoke(context.Background(), []byte(`{"q":"denim"}`))
	if err != nil {
		panic(err)
	}
	fmt.Println(tools[0].Name, "->", out)
}
Output:
arch_search -> 3 hits
Example (ToolSource)

ExampleTools_toolSource is the listChanged pattern: the SDK's handler re-runs Tools into a slice the agent serves through weft.ToolSource — no registry, no owned goroutine; the list is the caller's value and the loop refetches it every step.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"

	sdk "github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/weftgo/weft"
	"github.com/weftgo/weft/mcp"
	"github.com/weftgo/weft/wefttest"
)

func main() {
	srv := sdk.NewServer(&sdk.Implementation{Name: "remote", Version: "0"}, nil)
	srv.AddTool(&sdk.Tool{
		Name:        "search",
		Description: "Search.",
		InputSchema: json.RawMessage(`{"type":"object"}`),
	}, func(_ context.Context, _ *sdk.CallToolRequest) (*sdk.CallToolResult, error) {
		return &sdk.CallToolResult{Content: []sdk.Content{&sdk.TextContent{Text: "ok"}}}, nil
	})
	serverTransport, clientTransport := sdk.NewInMemoryTransports()
	go func() { _ = srv.Run(context.Background(), serverTransport) }()
	client := sdk.NewClient(&sdk.Implementation{Name: "demo", Version: "0"}, nil)
	sess, err := client.Connect(context.Background(), clientTransport, nil)
	if err != nil {
		panic(err)
	}

	// The SDK runs ToolListChangedHandler on its own goroutine and the
	// loop reads the source on the run's, so the slice lives under a
	// mutex: refresh writes it, the source reads it.
	var (
		mu      sync.Mutex
		current []*weft.ToolDef
	)
	refresh := func() {
		tools, err := mcp.Tools(context.Background(), sess)
		if err == nil {
			mu.Lock()
			current = tools
			mu.Unlock()
		}
	}
	refresh()
	// A real client sets ClientOptions.ToolListChangedHandler to call
	// refresh; the served list is whatever the last refresh produced.
	agt := weft.New(wefttest.Script(wefttest.Say("ready")),
		weft.Name("importer"),
		weft.ToolSource(func() []*weft.ToolDef {
			mu.Lock()
			defer mu.Unlock()
			return current
		}))
	res, err := agt.Generate(context.Background(), weft.Prompt("status?"))
	if err != nil {
		panic(err)
	}
	mu.Lock()
	n := len(current)
	mu.Unlock()
	fmt.Println(res.Text(), "tools fetched:", n)
}
Output:
ready tools fetched: 1

Types

type ImportError

type ImportError struct {
	Skipped []SkippedTool
}

ImportError is Tools' report of the tools it could not import. It travels beside the importable tools, not instead of them — the partial-result shape *core.RunError uses (ADR 0002): the caller reads it with errors.As and decides whether a skipped tool is a warning or a stop. Unwrap exposes each tool's cause, so errors.Is against a ParseSchema failure still works through the report.

func (*ImportError) Error

func (e *ImportError) Error() string

func (*ImportError) Unwrap

func (e *ImportError) Unwrap() []error

Unwrap returns each skipped tool's cause.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures one Tools call.

func Policy

func Policy(opts ...core.ToolOption) Option

Policy applies tool options to every imported tool, after the annotation defaults — so Sequential from a missing readOnlyHint cannot be undone here, and RequireApproval is added, never removed. Timeout, MaxResultBytes, RequireApproval and WrapTools apply as on any tool; StrictInput has no effect (a RawTool receives the model's raw arguments — validation belongs to the server). Every imported tool carries core.Origin("mcp") (the tools record's source); a core.Origin given here overrides it.

func Prefix

func Prefix(p string) Option

Prefix prepends p to every imported tool's name — "gh_" turns "search" into "gh_search" — so two servers' tools can share one agent; the remote call still uses the server's name.

type SkippedTool

type SkippedTool struct {
	Name  string
	Index int
	Err   error
}

SkippedTool is one tool Tools left out: its server-side name (empty when the name itself was the problem), its position in the listing, and why.

Directories

Path Synopsis
examples
client command
Command client consumes MCP servers as weft tools: it connects two in-memory servers concurrently under one deadline, imports their tools with per-server prefixes, registers them on a scripted agent, runs it, and prints the manifest — the imported tools' raw schemas show in it.
Command client consumes MCP servers as weft tools: it connects two in-memory servers concurrently under one deadline, imports their tools with per-server prefixes, registers them on a scripted agent, runs it, and prints the manifest — the imported tools' raw schemas show in it.
server command
Command server exposes a weft agent as an MCP server over stdio — the getting-started agent plus its lookup tool, both directions of §7 in one process:
Command server exposes a weft agent as an MCP server over stdio — the getting-started agent plus its lookup tool, both directions of §7 in one process:

Jump to

Keyboard shortcuts

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