mcpserver

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

Documentation

Overview

Package mcpserver exposes the daemon's research surface over MCP (§5.1).

Separate from internal/daemon so that package stays transport-only: the socket and its permissions are one concern, what a caller can ask for is another, and this one is testable over an in-memory transport with no socket at all.

The tools are asynchronous by design (§5.3). research.report returns a session id and nothing else; the work continues in the daemon long after the calling agent's context is gone, and the caller polls research.status the way it would wait on a CI job.

Index

Constants

View Source
const (
	MaxSourcesCeiling = 25
	MaxDepthCeiling   = 5
)

Ceilings on what a caller may ask for per lead and per session.

Generous — a legitimate agent will never reach them — and finite, which is the point: every value here is multiplied into a resource bound.

View Source
const AllClaims = 100_000

AllClaims is the limit to pass when every claim of a session is wanted.

store.ListClaims treats a non-positive limit as 500, which is a sensible default for a display and a wrong one here: with 600 claims recorded, edge_add refused a pair id for claim 550 as "not in session" — a claim mole itself had stored minutes earlier. The eval reads whole sessions with the same magnitude.

View Source
const AskAllowance = 50_000 // $0.05

AskAllowance bounds what one research.ask call may spend, in micro-dollars.

§13's promise is that an ask is cheap: one model call against claims already paid for. The allowance is what makes that true rather than aspirational — without it an ask draws on whatever the session has left, and a session that stopped early with most of its budget intact could spend it all answering one follow-up.

View Source
const DefaultAskTimeout = 5 * time.Minute

DefaultAskTimeout bounds an ask's wall clock. One model call; five minutes is generous for it and short enough that a wedged provider does not pin a reservation until the abandonment sweep. Enforced on the context in Ask.

View Source
const DefaultSessionsListed = 20

DefaultSessionsListed is the page size for research.sessions.list.

View Source
const DefaultToolkitBudgetUSD = 1_000_000 // micros

DefaultToolkitBudgetUSD is what a session gets when the caller names no budget: one dollar, which is roughly 125 Tavily queries.

View Source
const MaxClaimsReturned = 200

MaxClaimsReturned caps how many claims research.result hands back.

The result goes straight into a coding agent's context window, and nothing upstream bounds how many claims a session gathers. A cap the caller can see — reported as Truncated with the real Total — beats both silently sending five thousand and silently sending two hundred of them.

View Source
const MaxDatasetRows = 200

MaxDatasetRows bounds what one mole.dataset call returns.

The merged table goes into an agent's context window twice over — as markdown and as structured rows — so it needs the same bound the other list tools have. `mole dataset` exports the whole thing.

View Source
const MaxEdgesReturned = 500

MaxEdgesReturned caps the edges alongside them, for the same reason.

View Source
const MaxFetchChars = 120_000

MaxFetchChars bounds the text one fetch returns.

The result goes straight into an agent's context window, and nothing about a web page bounds its length. Truncation is reported rather than silent, because a quote from beyond the cut will fail verification later and the caller needs to know that is why.

View Source
const MaxPairsReturned = 50

MaxPairsReturned bounds one retrieval. Pairs go straight into an agent's context window, and a session with two hundred claims has thousands of candidate pairs.

View Source
const MaxRowsPerCall = 200

MaxRowsPerCall bounds one batch. Larger batches are not refused work, only split, and a bound keeps one call from holding a write transaction open over an arbitrary amount of parsing.

View Source
const MaxSearchResults = 10

MaxSearchResults bounds one search for the same reason.

View Source
const MaxTitleChars = 300

MaxTitleChars bounds a page-supplied title.

Titles reach the model OUTSIDE the fence — they are metadata a caller cites by, not document text — so the only protection available is that they are short. Same bound as a search snippet, for the same reason.

View Source
const MaxToolkitCalls = 500

MaxToolkitCalls bounds how many searches and fetches one toolkit session may make.

Autonomous mode is bounded by money: every call is on a lead, and leads stop when the budget does. Here the money bound is weak on purpose — a search and a fetch cost mole no model tokens, so in a token-budget session the ledger would never stop an agent that loops. This is the ceiling that does, and it is §8.5's existing one rather than a new mechanism.

Variables

This section is empty.

Functions

func New

func New(d Deps) *mcp.Server

New builds the MCP server with the tool surface attached.

One server for the whole daemon, connected to each accepted connection separately. The tools close over shared state, so everything they touch is concurrent by construction.

Types

type AskIn

type AskIn struct {
	SessionID string `json:"session_id" jsonschema:"the session whose research should be queried"`
	Question  string `json:"question" jsonschema:"the follow-up question to answer from that session's claims"`
}

type AskOut

type AskOut struct {
	SessionID string `json:"session_id"`
	Question  string `json:"question"`
	Answer    string `json:"answer"`

	// Claims are the ones the answer is built from, so a caller can weigh the
	// evidence rather than the prose (§5.3).
	Claims []Claim `json:"claims"`
	// Citations maps each [n] in the answer to its source.
	Citations []Citation `json:"citations"`

	// AskSessionID is the small session this answer was charged to. Present so
	// the cost is traceable: it will not appear against the session that was
	// queried, whose accounts are closed.
	AskSessionID string `json:"ask_session_id,omitempty"`
	Spent        int64  `json:"spent"`
	// Degraded says why the answer is not synthesized prose, when it is not.
	Degraded string `json:"degraded,omitempty"`
}

type Budget

type Budget struct {
	Unit   string `json:"unit" jsonschema:"the budget unit: usd or tokens"`
	Amount string `json:"amount" jsonschema:"how much to spend: dollars like 2.50 for usd, a whole number of tokens for tokens"`
}

Budget is the amount a session may spend. Both fields are required: §8 makes the unit semantically load-bearing, and a bare number is ambiguous between dollars and tokens.

type CancelOut

type CancelOut struct {
	SessionID string `json:"session_id"`
	Cancelled bool   `json:"cancelled"`
	Note      string `json:"note,omitempty"`
}

type Citation

type Citation struct {
	N           int      `json:"n"`
	Source      string   `json:"source"`
	Quotes      []string `json:"quotes,omitempty"`
	PublishedAt string   `json:"published_at,omitempty"`
}

Citation is one numbered source behind an answer.

Quotes plural: several claims can cite one source, and §13 asks for the verified span per citation so a reader can check the answer without re-fetching the page.

type Claim

type Claim struct {
	ID          string  `json:"id"`
	Text        string  `json:"text"`
	Source      string  `json:"source"`
	Quote       string  `json:"quote,omitempty"`
	PublishedAt string  `json:"published_at,omitempty"`
	Confidence  float64 `json:"confidence"`
	Grounded    *bool   `json:"grounded,omitempty"`
}

Claim is one finding, in the shape §5.3 says an agent caller wants: sourced, dated, quoted, and scored, rather than a paragraph.

type Deps

type Deps struct {
	// Supervisor starts and cancels sessions.
	Supervisor *session.Supervisor
	// Store answers every read. Deliberately not the supervisor: the store is
	// authoritative and survives a restart, while the supervisor only remembers
	// the sessions it is running plus a bounded tail of finished ones. A status
	// query that worked before a daemon restart and failed after would be the
	// worst possible behaviour for a caller told to poll.
	Store store.Store

	// MaxSessionUSD and MaxSessionTokens cap a single session's budget in their
	// respective units. Zero means no ceiling. See config.MaxSessionUSD for why
	// these exist, and checkCeiling for why having only one set is refused.
	MaxSessionUSD    int64
	MaxSessionTokens int64

	// Version is the binary's version, reported to MCP clients in the server
	// handshake. Empty reports "dev".
	Version string

	// LLM answers research.ask. Nil disables synthesis: an ask then returns the
	// relevant claims with citations and no prose, which is the honest response
	// when no call can be made.
	LLM llmProvider
	// Pricing turns an ask's token usage into a ledger cost. Nil takes the
	// default table.
	Pricing *pricing.Table

	// AskTimeout bounds one research.ask. Zero takes DefaultAskTimeout.
	AskTimeout time.Duration

	// Workers is how many leads each session runs at once. Zero takes
	// executor.DefaultWorkers.
	Workers int

	// Defaults applied when a call does not specify.
	MaxSources int
	MaxDepth   int
	MaxLeads   int
	Timeout    time.Duration

	// Search, Fetch and Extract back toolkit mode. Nil leaves the matching tool
	// reporting that it is not configured rather than failing at call time.
	Search  search.Provider
	Fetch   fetch.Fetcher
	Extract extract.Extractor

	// Connectors is the registered local data, for the toolkit's aggregate tools.
	// Nil leaves them reporting that nothing is registered.
	Connectors interface{ List() []connector.Connector }
	// Gate tunes the aggregation gate. The zero value is §12.1's defaults.
	Gate gate.Options

	// Toolkit registers the toolkit tool surface. Off by default: those tools cost
	// every client their definitions in its context window, and a caller who wants
	// autonomous research should not read past them to find research.report.
	Toolkit bool

	Log *slog.Logger
}

Deps is what the tools operate on.

func (Deps) Ask

func (d Deps) Ask(ctx context.Context, in AskIn) (AskOut, error)

ask answers a new question from a finished session's graph (§13).

The ask runs as its OWN session, with mode "ask" and a small budget of its own, reading the source session's claims. That is not indirection for its own sake — it is forced, and the reason is worth stating.

§8 refuses a reservation against a terminal session, and rightly: a finished session's ledger is settled and reconciled, and adding cost rows to it later rewrites a closed account. So an ask cannot be charged to the research it queries. Giving it a session of its own keeps every §8 invariant intact — every model call belongs to a live session, every cost row has a reservation, the source session's accounts stay closed — and has the side benefit that an ask shows up in sessions.list with its own cost, which is what a person wondering where the money went needs to see.

This is also what core.ModeAsk was reserved for. §13 describes it as "listed in rev 1's Session.Mode but never defined"; this defines it.

Exported because `mole ask` is the same operation from a terminal instead of an MCP client, and the budgeting above is the part that must not be written twice. The CLI builds a Deps with no Supervisor — an ask never starts one — and calls this directly.

type Edge

type Edge struct {
	From      string  `json:"from"`
	To        string  `json:"to"`
	Kind      string  `json:"kind"`
	Weight    float64 `json:"weight,omitempty"`
	Rationale string  `json:"rationale,omitempty"`
}

Edge is one relation in the claim graph (§11.2).

type ListIn

type ListIn struct {
	Limit int `json:"limit,omitempty" jsonschema:"how many recent sessions to return; omit for 20"`
}

type ListOut

type ListOut struct {
	Sessions []SessionSummary `json:"sessions"`
}

type ReportIn

type ReportIn struct {
	Prompt string `json:"prompt" jsonschema:"the research question"`
	Budget Budget `json:"budget" jsonschema:"what this session may spend"`

	MaxSources int `json:"max_sources,omitempty" jsonschema:"sources to read per lead; omit for the daemon default"`
	MaxDepth   int `json:"max_depth,omitempty" jsonschema:"rounds of follow-up questions the planner may add; omit for the daemon default"`
}

type ReportOut

type ReportOut struct {
	SessionID string `json:"session_id"`
	Status    string `json:"status"`
	Unit      string `json:"budget_unit"`
	Budget    int64  `json:"budget"`
	Note      string `json:"note,omitempty"`
}

type ResultOut

type ResultOut struct {
	SessionID string `json:"session_id"`
	Status    string `json:"status"`
	ReportMD  string `json:"report_md"`

	Claims []Claim `json:"claims"`
	Edges  []Edge  `json:"edges"`

	// TotalClaims and TotalEdges are what the session holds; the arrays above may
	// be shorter. Reported so a truncated result is visibly truncated — silently
	// returning a prefix is how a caller concludes a session found less than it
	// did.
	TotalClaims int  `json:"total_claims"`
	TotalEdges  int  `json:"total_edges"`
	Truncated   bool `json:"truncated"`

	Note string `json:"note,omitempty"`
}

type SessionRef

type SessionRef struct {
	SessionID string `json:"session_id" jsonschema:"the id returned by research.report"`
}

type SessionSummary

type SessionSummary struct {
	SessionID string `json:"session_id"`
	Status    string `json:"status"`
	Prompt    string `json:"prompt"`
	Unit      string `json:"budget_unit"`
	Budget    int64  `json:"budget"`
	Spent     int64  `json:"spent"`
	Running   bool   `json:"running"`
}

type StatusOut

type StatusOut struct {
	SessionID string `json:"session_id"`
	Status    string `json:"status"`
	Prompt    string `json:"prompt"`

	Unit      string `json:"budget_unit"`
	Budget    int64  `json:"budget"`
	Spent     int64  `json:"spent"`
	Remaining int64  `json:"remaining"`

	Claims   int  `json:"claims"`
	Verified int  `json:"claims_verified"`
	Edges    int  `json:"edges"`
	Running  bool `json:"running"`
}

Jump to

Keyboard shortcuts

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