Documentation
¶
Overview ¶
coverage-ignore: MCP prompt handlers - tested via integration tests
coverage-ignore: MCP resource handlers - tested via integration tests
coverage-ignore: MCP server - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: MCP tool handlers - tested via integration tests
coverage-ignore: file watcher - now managed by workspace
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Deps ¶
type Deps struct {
// Store is the READ handle for every MCP read surface — tools,
// resources, prompts, export and analyze alike. It is deliberately
// the narrow [GraphReader], not `store.Store`: writes go through
// EntityManager, so MCP never needs the wide composite, and typing
// the field this way makes an ungated raw read *unavailable* rather
// than merely discouraged (the TKT-80EWGM "make the mistake
// impossible" pattern, applied to reads).
//
// The wiring site decides what this is. `rela mcp` (stdio) passes the
// raw store — the filesystem is the trust boundary there, so a gate
// would defend nothing. A networked wiring passes a
// visibility-wrapped reader that resolves the ctx principal per call.
// Either way the handlers are identical; gating is entirely a wiring
// decision (DEC-ZBI39P).
Store GraphReader
Meta *metamodel.Metamodel
Tracer tracer.Tracer
Searcher search.Searcher
Validator validator.Validator
EntityManager EntityWriter
Config config.Loader
LuaWriteDeps lua.WriteDeps
LuaCache *lua.Cache
Watcher Watcher
ProjectRoot string
}
Deps is the focused bundle of backend services the MCP server needs. Every field is a domain type — the server holds no reference to any composition-root aggregate, so `internal/mcp` does not import `internal/appbuild` (enforced by arch-lint). The wiring site (`internal/cli`) constructs a Deps from focused services and supplies it to NewServer; tests build a Deps literal directly.
ProjectRoot is the absolute project root, used by the lua tools to resolve relative script paths. It is the only piece of the project context MCP consumes — passing the string instead of a `*project.Context` keeps that type from leaking into MCP test stubs.
type EntityWriter ¶
type EntityWriter interface {
CreateEntity(ctx context.Context, e *entity.Entity, opts entity.CreateOptions) (*entity.CreateResult, error)
PatchEntity(ctx context.Context, id string, p entity.Patch) (*entity.UpdateResult, error)
DeleteEntity(ctx context.Context, id string, cascade bool) (*entity.DeleteResult, error)
RenameEntity(
ctx context.Context, oldID, newID string, opts entity.RenameOptions,
) (*entity.RenameResult, error)
CreateRelation(
ctx context.Context, from, relType, to string, opts entity.RelationOptions,
) (*entity.Relation, error)
DeleteRelation(ctx context.Context, from, relType, to string) error
}
EntityWriter is the write capability MCP requires — the exact set its tool handlers call, declared here at the CALL SITE for the same reason GraphReader is: `entitymanager`'s own interface was a nine-method producer-side type that MCP has no business holding in full (TKT-IVSJV6). The wiring site supplies the project's *entitymanager.Manager, which satisfies this structurally.
Three of the nine are absent because no MCP tool invokes them: UpdateEntity (the entity tool patches — it names the properties it touched rather than holding the whole record, TKT-80EWGM), ValidateCreate (an advisory dry-run the data-entry form path uses), and UpdateRelation.
Every method here still routes through the manager, so ACL, audit and automations apply exactly as they do on any other write path — narrowing the interface removes methods, never gates.
type GraphCounter ¶
type GraphCounter interface {
CountEntities(ctx context.Context, q store.EntityQuery) (int, error)
CountRelations(ctx context.Context, q store.RelationQuery) (int, error)
}
GraphCounter is the structural half of GraphReader: type-level tallies that name no individual row. Kept as its own interface so a wiring site can compose a gated row-reader with a raw counter without either pretending to be the other.
type GraphReader ¶
type GraphReader interface {
GraphCounter
GetEntity(ctx context.Context, id string) (*entity.Entity, error)
ListEntities(ctx context.Context, q store.EntityQuery) iter.Seq2[*entity.Entity, error]
GetRelation(ctx context.Context, from, relType, to string) (*entity.Relation, error)
ListRelations(ctx context.Context, q store.RelationQuery) iter.Seq2[*entity.Relation, error]
}
GraphReader is the read capability MCP requires of its store — the exact set the handlers call, declared here at the CALL SITE rather than reused from `store.Store`, which is a ten-interface composite (CRUD, attachments, watching, transactions) MCP has no business holding.
It is split deliberately. The three ENTITY/RELATION reads are the gated surface: they return rows, so a wiring may substitute a decorator that hides some. The two COUNTS are GraphCounter, kept separate because a count is structural — it discloses how many rows of a declared type exist, not which ones — and `internal/dataentry` already draws this exact line (`analyzeService.relCounts` is "raw (ungated) on purpose").
`store.Store` satisfies the whole thing structurally, so the stdio wiring passes one unchanged; a visibility decorator satisfies the gated half, which is the point.
type Option ¶
type Option func(*Server)
Option configures a Server at construction.
func WithPrincipal ¶
WithPrincipal stamps p onto every tool-handler ctx via a server middleware so downstream audit records are correctly attributed. Applies to every registered tool — including lua_eval / lua_run / any future write tool — because the middleware runs ahead of all handlers (registration-time wrapping, not per-handler opt-in).
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server wraps the MCP server with rela-specific state.
TODO(TKT-N0IKN9): Server started this arc over the 40-method load line; it is under it now (25 methods). The directive stays pinned to the actual count so extraction gains cannot silently erode; ratchet it further as the remaining handler clusters move out.
48 → 49: Server.HTTPHandler (TKT-BDG8U9). It belongs on Server — it exposes THIS server over a second transport, the peer of Server.Serve — and it is the only method the remote endpoint added: the stateless-transport choice lives inside it, and the wiring site holds an http.Handler rather than reaching for the SDK.
49 → 38 (TKT-YUETL7): the type-name helpers moved to typeResolver (they need only the metamodel) and the trace/export tool handlers moved to traceHandler / exportHandler (store + tracer, and store + resolver, respectively). Each is a field below, wired from Deps in NewServer; registerTools points the affected AddTool lines at the field's methods.
38 → 25 (TKT-MGNE5L): the lua tools moved to luaHandler (the sole user of LuaWriteDeps / LuaCache / ProjectRoot), the schema tools + resource reads to schemaResourceHandler (store + metamodel), and the prompt handlers to promptHandler (store + metamodel + tracer + resolver). register* stay on Server and point at the fields' methods. The remaining handlers genuinely span deps — the next TKT-N0IKN9 slice.
25 → 27 (TKT-NU247U): Server.ReloadDeps and the unexported deps accessor. Both are the reload seam itself — ReloadDeps is the capability, and deps() is what makes every handler read the CURRENT snapshot instead of one baked in at construction. The six handler-group accessors that would also have landed here are free functions ([group], [setDeps], [bind]) precisely to keep this number from moving further; ratchet it back down with the remaining handler clusters.
func NewServer ¶
NewServer creates a new MCP server for a rela project. Returns an error if WithPrincipal was not supplied — silently degrading to `unknown/unknown` audit attribution would be an invisible production bug (CLAUDE.md "constructors reject nil required fields"). Tests must pass a non-zero Principal too — use any non-empty `principal.Principal{User: ..., Tool: ...}`.
func (*Server) HTTPHandler ¶
HTTPHandler returns an http.Handler serving this server over Streamable HTTP, for mounting inside an existing router (TKT-BDG8U9). The caller owns authentication, ACL and routing; this method owns only the MCP transport.
**Stateless is required, not a tuning choice.** Protocol revision 2026-07-28 is reachable ONLY on a stateless server in the go-sdk — a session-bearing one negotiates down to 2025-11-25, because the newer revision removes sessions entirely. Consequences the caller inherits:
- GET and DELETE get 405; only POST carries messages.
- Server→client requests are rejected (there is no channel to answer on).
- Notifications reach the client only within an in-flight request.
That last point is why the file watcher is pointless on this transport and a caller should pass a no-op Watcher: `resources/list_changed` has no stateless equivalent. Remote clients re-read on demand and see fresh data, because every read goes to the store.
The returned handler serves THIS server for every request, so per-request state must travel on the ctx rather than be baked in here. That is exactly how identity works: the transport passes the *http.Request ctx through to handlers, and Server.principalMiddleware preserves a principal already stamped there in preference to the construction-time one.
func (*Server) ReloadDeps ¶
ReloadDeps atomically republishes the server against a freshly built dependency bundle, so subsequent requests observe the new metamodel and the services derived from it.
This is how `rela mcp` picks up a `schema.yaml` edit without a restart (TKT-NU247U): the wiring site rebuilds the metamodel-derived service stack against the SAME store and searcher, then hands the new Deps here. The registered tool/resource/prompt SET is unchanged — only what the handlers read through — so no capability renegotiation is involved.
An invalid bundle is refused and the previous one stays published: a reload driven by a file watcher must never be able to leave a running server without a usable metamodel.
Safe to call while requests are in flight. A request that has already resolved the snapshot completes against it; the next one sees the new bundle.
type Watcher ¶
type Watcher interface {
Start(onChange func()) error
Stop()
Pause()
Resume()
}
Watcher is the narrow file-watching capability MCP requires from its wiring site. Start arms the watcher with an opaque "something changed" callback; Pause / Resume temporarily suppress callbacks while in-process writes happen (e.g. entity rename). The wiring site supplies an adapter that translates these calls into the underlying filesystem watcher.