MCP server
Fallen-8 speaks to programs (the REST API) and to humans (F8 Studio). The MCP server is its surface for AI agents: a separate service that exposes Fallen-8 as typed Model Context Protocol tools any MCP client, Claude Code, Claude Desktop, IDE agents, other vendors’ agents, can discover and call.
It is a separate deployable: its own project (fallen-8-mcp), its own process and
container image. It never loads the engine; it bridges to a reachable Fallen-8 over the
existing REST API, so one MCP server can front a local scratch graph or a shared instance on
another host. A bug in the agent surface cannot take the database down.
Running it
Section titled “Running it”With the compose environment (the default). The MCP server comes up with the rest of the
environment on http://localhost:8090, anonymous and read-only, matching this
environment’s no-auth-in-the-way posture (the fallen8 service itself runs with no API key
here). Nothing extra to run:
npm run env:up# f8-mcp is on http://localhost:8090, fronting the fallen8 service, read-only.This local-dev posture is not for a serious deployment; see “Securing it” below.
Securing it for a real (off-box) setup. Everything is env-var configurable on the f8-mcp
service, so a serious setup locks it down without code changes: set an auth mode + credential,
and opt into only the tiers you need. Via the compose env, with the token exported rather
than inlined (nothing prints it back, and you need it again to register a client):
export F8_MCP_TOKEN=$(openssl rand -hex 32)# static bearer + writes enabled, still on the compose network:F8_MCP_AUTH_MODE=StaticToken F8_MCP_ENABLE_WRITE=true npm run env:upF8_MCP_ENABLE_ADMIN and F8_MCP_ENABLE_CODE open the other two tiers the same way (the full
list of compose variables is in Running Fallen-8).
Or run the image standalone against a Fallen-8 on another host, fully credentialed. Every tagged
release publishes it as ghcr.io/cosh/fallen-8-core-mcp; build it yourself only for an
unreleased tree (the Dockerfile’s context is the repo root):
docker pull ghcr.io/cosh/fallen-8-core-mcp:latest # or build: docker build -f fallen-8-mcp/Dockerfile -t fallen-8-mcp .
docker run --rm -p 8090:8090 \ -e Fallen8Target__BaseUrl=https://graph.example:8443 \ -e Fallen8Target__ApiKey=$F8_KEY \ -e Mcp__Auth__Mode=StaticToken -e Mcp__Auth__StaticToken=$MCP_TOKEN \ -e Mcp__Security__BindAddress=0.0.0.0 -e Mcp__Security__AllowRemoteAccess=true \ fallen-8-mcpFor OAuth 2.1 instead of a static token, set Mcp__Auth__Mode=OAuth +
Mcp__Auth__Issuer/Mcp__Auth__Audience (see “Authentication”). The server fails closed:
a non-loopback bind refuses to start anonymously unless you explicitly opt in
(Mcp__Security__AcceptAnonymousRemote=true, which the demo compose sets and a real one must not).
It also refuses to start with Mode=StaticToken and no token, or Mode=OAuth and no audience,
instead of coming up and rejecting every call.
Local development over stdio (no network transport, loopback Fallen-8). The bridge targets
http://localhost:8080 unless told otherwise, so point it at whatever your Fallen-8 listens on:
a bare dotnet run of the API is on http://localhost:5000, the compose environment on :8080.
Fallen8Target__BaseUrl=http://localhost:5000 dotnet run --project fallen-8-mcp -- --stdioAs a .NET tool, no checkout needed. The server also ships on nuget.org as the tool package
fallen-8-mcp (from the first release after it was added), so the .NET 10 SDK’s dnx runs it
without installing anything; this is the line the Claude Code plugin’s native tier launches:
Fallen8Target__BaseUrl=http://localhost:5000 dnx fallen-8-mcp --stdioIt bridges to Fallen8Target__BaseUrl (default http://localhost:8080) and reads the same
environment variables as the container; Mcp__Transport=stdio is honoured too, for a launcher
that cannot pass the flag.
Probes. On the HTTP transport, GET /healthz is liveness (the process is up) and
GET /readyz is readiness: one bridged GET /status under a short deadline
(Mcp:Readiness:TimeoutSeconds, default 3), answering 503 with a reason when the Fallen-8 is
unreachable, times out, or requires an API key and rejected the configured one. Both are outside
the origin, bearer and rate-limit checks, so an orchestrator can poll them bare; the compose
environment’s f8-mcp healthcheck uses /readyz, which is what f8-agents waits for.
Connecting a client
Section titled “Connecting a client”Claude Code, over Streamable HTTP. The default dev server is anonymous:
claude mcp add --transport http fallen8 http://localhost:8090When you have secured it with a static bearer (above), add the header:
claude mcp add --transport http fallen8 http://localhost:8090 \ --header "Authorization: Bearer $F8_MCP_TOKEN"For stdio, point the client at dotnet run --project fallen-8-mcp -- --stdio (or the built
binary, or dnx fallen-8-mcp --stdio) and set Fallen8Target__BaseUrl in its environment.
The Claude Code plugin. cosh/fallen-8-claude-plugin
packages the connection plus skills for working with a Fallen-8 from Claude Code:
claude plugin marketplace add cosh/fallen-8-claude-pluginclaude plugin install fallen-8@fallen-8It reads FALLEN8_URL, the MCP server’s URL (http://localhost:8090 for the compose
environment), and its native tier launches the tool package above. The plugin has its own
repository and documentation; this page states no more about it than those two facts.
The tools
Section titled “The tools”Thirteen consolidated, capability-oriented tools cover the whole surface (not one per REST route),
so a client loads few schemas and each result stays compact. Nearly every tool takes an optional
namespace (a Fallen-8 hosts many isolated graphs; it defaults to default); f8_namespace
names its target directly instead, and f8_admin’s list_savegames/load are Fallen-8-level.
| Tier | Tool | What it does |
|---|---|---|
| read | f8_overview |
Discover a graph: omit namespace to list namespaces; set it for counts, index count, available algorithms/index plugins, and the capability state an agent plans around: embedding (embeddingEnabled, embeddingBackend, embeddingModel, embeddingDimension), chat, ingestion (ingestionEnabled, doclingConfigured, doclingReachable, the bound ingestionVectorIndexId/ingestionFulltextIndexId/ingestionEmbeddingName), NLP (nlpEnabled, nlpReachable) and auth. detail:"statistics" adds the full graph-shape snapshot (label/key cardinalities, degree distribution, and the index inventory itself). A namespace this process did not load is reported as not loaded, with no counts rather than zeros, and the summary names the fix (f8_admin op:'activate'). Start here. |
| read | f8_get |
Fetch a vertex/edge by id with optional neighbourhood (include) and property projection (fields). Compact by default, scalar values only, vectors omitted. |
| read | f8_search |
Find elements: mode = index | property (un-indexed, one named key) | properties (un-indexed contains scan across every property value) | fulltext | vector | semantic. kind and label restrict the hits (ignored by fulltext). Returns ids (+score); fields enriches with properties. Paginated (limit/cursor). |
| read | f8_paths |
Find paths between two vertices, unfiltered or by a registered stored query. Knobs: algorithm (free-form: BLS by default, DIJKSTRA, or any registered Path plugin the overview reports), maxDepth (default 7), maxResults, maxPathWeight, timeBudgetSeconds, and a semantic block (queryVector or queryText, embeddingName, metric, minScore, costBySimilarity) forwarded as sent, so the server’s own validation and its one-owner-per-slot 400s apply. An empty result can also mean an internal traversal limit was hit, so it is not proof no path exists. |
| read | f8_storedquery |
The stored-query library: list/get (with compileState and, on get, the stored specification as an object); delete needs the write capability; register compiles C# fragments and needs the code capability. What it registers is what storedQuery on f8_paths/f8_subgraph invokes by name. |
| read | f8_analytics |
Run a whole-graph algorithm (PageRank, WCC, communities, centrality, triangle-count), or omit algorithm to list them. Optional per-run knobs: vertexLabel, edgePropertyId, direction, maxResults (default 25), maxIterations, and a numeric parameters map (e.g. {"DampingFactor": 0.85}). |
| read | f8_plugins |
The per-namespace plugin registry: list/get/invoke (a graph function by name); delete needs the write capability; register_algorithm/register_function (from C# source) need the code capability. An agent can run every registered plugin category: a graph function through invoke here, and a registered algorithm by naming it in the algorithm knob of f8_paths, f8_subgraph or f8_analytics. |
| read | f8_documents |
Unstructured ingestion: search (fused dense+lexical chunk retrieval; hits are vertex ids), list, get, binding (the index-binding state), entities (the deduplicated entity network); ingest_text/delete/bind need the write capability. Binary file upload stays REST-only (base64 through tool calls wastes tokens). |
| write | f8_mutate |
One transactional mutation: create_vertex, create_edge, create_vertices, create_edges (atomic batch creates), set_property, set_properties (an atomic batch of updates, each {id, key, value} or {id, key, remove: true}), remove_property, remove_element, remove_elements (an atomic batch of ids), set_embedding. Property values are JSON-native. Success means the transaction applied. The batch creates return the assigned ids; the single creates do not (find them with f8_search), and the property and removal ops are no-ops for an absent id, so success does not prove the element existed. |
| write | f8_index |
The index lifecycle: create (any pluginType the overview lists, with JSON-native options such as {dimension, metric, embeddingName} for a VectorIndex), add/add_many, add_vector, remove_element/remove_key, backfill from a property, delete. The REST routes answer a bare false for a taken id, an unknown type or a missing index; the tool reports those as errors whose message names every cause and says the status is the bridge’s reading. |
| write | f8_subgraph |
Define a subgraph from a stored template, a code-free patterns list (Vertex/Edge/VariableLengthEdge steps) and/or a semantic block, or inline filters when the code capability is on. A pattern step carrying a C# fragment needs the code capability too. |
| write | f8_namespace |
Create, rename, or drop a namespace. |
| admin | f8_admin |
Durability, maintenance & instance configuration: save, list_savegames, load (by save-game id, optionally restoring a single restoreNamespace member), activate (load a namespace this process skipped at startup, see namespaces; it needs an explicit namespace, since activating default is always a no-op), trim, tabula_rasa, and get_settings/set_settings over the configuration surface (get_settings is the first thing to read when a limit or a capability refuses a call; a write is validated as a whole batch and most keys take effect only after a restart, which the result says per key). trim/tabula_rasa are fire-and-forget: they report “enqueued”, never “applied”. |
Tools carry MCP annotations so clients can surface the right confirmation UX. The five purely-read
tools (f8_overview, f8_get, f8_search, f8_paths, f8_analytics) are readOnlyHint, and
so is f8_storedquery until write or code is on; f8_namespace (drop), f8_admin
(load/trim/tabula_rasa), f8_index (delete, backfill with replace) and also
f8_plugins/f8_documents are destructiveHint, the last two because they host write/code ops
behind a per-op gate, so a client may ask for confirmation even on their read ops. Annotations
are hints: the real enforcement is server-side tier gating.
Tiers and the code capability
Section titled “Tiers and the code capability”Tools are grouped by the same opt-in tiers Fallen-8 uses everywhere:
- read (on by default): discovery, fetch, search, paths, analytics, plus the read ops of the plugin registry, the stored-query library and documents.
- write (
Mcp:Tools:EnableWrite): mutations, the index lifecycle, subgraph define, namespace lifecycle, and the write ops inside the read-tier tools (deleteonf8_pluginsandf8_storedquery;ingest_text/delete/bindonf8_documents), which appear in those tools’oplist only once write is on. - admin (
Mcp:Tools:EnableAdmin): save/load/trim/tabula_rasa. - code (
Mcp:Tools:EnableCode): does not add tools; it widens existing ones with C# source: inline filter/cost fragments onf8_paths/f8_subgraph(including a fragment inside apatternsstep), theregisterop onf8_storedquery, and theregister_algorithm/register_functionops onf8_plugins(whole-type plugin source). Off by default so the MCP surface stays token-frugal and does not invite arbitrary C# from agents; the target Fallen-8 always accepts the equivalent (auth + the plugin gate permitting), so this is purely an MCP-side exposure choice.
A disabled tier’s tools are absent from the tool list and rejected if called anyway.
Authentication
Section titled “Authentication”Three modes (Mcp:Auth:Mode), additive:
- None: anonymous. Safe only on loopback / a private network. The server binds loopback
by default and refuses to start on a network-reachable bind while anonymous (unless you
explicitly set
Mcp:Security:AcceptAnonymousRemote=true). - StaticToken: a shared bearer token (
Authorization: Bearer …). Pragmatic for a network you mostly trust; the compose default is anonymous, so opt in withF8_MCP_AUTH_MODE=StaticToken+F8_MCP_TOKEN. - OAuth: a standards-track OAuth 2.1 resource server that validates JWT access tokens from
your authorization server (audience binding is mandatory), publishes
RFC 9728 protected-resource metadata at
/.well-known/oauth-protected-resource, and maps scopes to tiers fail-closed:f8:write/f8:admin/f8:codeeach need both the scope and the server-side tier flag.
The caller’s credential is never forwarded to Fallen-8: the MCP server holds one downstream credential (the Fallen-8 API key) and presents only that. TLS for the MCP endpoint itself is the deployment’s job (terminate at a proxy, or configure Kestrel certificates); the server warns if auth is on over a non-loopback cleartext bind.
Origin validation (DNS-rebinding protection) and a fixed-window request rate limiter guard the
HTTP transport. A request that sends no Origin header passes validation, deliberately, because
MCP clients are not browsers; /healthz and the protected-resource metadata path stay open. The
limiter is on by default (600 requests per 60 seconds) and
Mcp:Security:RateLimit:PermitPerWindow=0 turns it off.
Token economy
Section titled “Token economy”Agents pay for every tool schema and every byte returned, so the surface is deliberately
frugal: few flat schemas, JSON-native inputs (you never write .NET type names), compact
results (scalar property values by default, vector values omitted, long strings truncated),
and hard pagination caps. f8_overview answers “what is here and what can I do” in one call.
The bridge infers a property/literal’s type from the JSON value, string → System.String,
integer → System.Int32/64, real → System.Double, bool → System.Boolean, which covers the
common cases; typed comparisons against DateTime/decimal properties are not yet inferred
(embeddings are written via f8_mutate set_embedding, not as a typed property). Rich results
ride in the tool result’s structuredContent; clients must consume that field (the content
block is only a one-line summary).
When a call fails
Section titled “When a call fails”Every failure comes back the same way: an isError result whose single text line is
{status} {title}: {detail}, normalized from whatever shape Fallen-8 returned. A transient
failure the agent may retry with backoff (429/503-style) carries a (retryable) suffix. An
unknown tool name, or one whose tier is off, answers 404 Unknown or disabled tool with a hint
to enable the tier (and, under OAuth, hold its scope). Anything unexpected is flattened to a
generic 500 Internal error so nothing sensitive leaks, and the downstream Fallen-8 API key is
never echoed to the caller.
Security guidance
Section titled “Security guidance”Run agent-facing servers read-only unless you have a concrete need for writes. An agent can
be talked (via prompt injection) into destructive calls, so keep write/admin/code tiers off by
default, rely on the destructiveHint confirmation UX in your client, and treat the code
capability (which runs C# fragments as the Fallen-8 process) as a trusted, deliberate choice.
Configuration reference
Section titled “Configuration reference”| Setting | Meaning |
|---|---|
Mcp:Transport |
http (default) or stdio |
Mcp:Port |
HTTP port (default 8090) |
Mcp:Security:BindAddress |
Kestrel bind (loopback by default; 0.0.0.0 in a container) |
Mcp:Security:AllowRemoteAccess / AcceptAnonymousRemote |
accept remote callers / allow anonymous remote (fail-closed override) |
Mcp:Security:Origins |
allowed cross-origins (loopback allowed by default) |
Mcp:Security:RateLimit:PermitPerWindow / WindowSeconds |
fixed-window request throttle (default 600 requests per 60 seconds; 0 disables the limiter) |
Mcp:Tools:EnableWrite / EnableAdmin / EnableCode |
tier / capability opt-ins (all off by default) |
Mcp:Readiness:TimeoutSeconds |
deadline of the one bridged call GET /readyz makes (default 3, clamped like the target timeout); past it the probe answers 503 naming this setting |
Mcp:Auth:Mode |
None | StaticToken | OAuth |
Mcp:Auth:StaticToken |
the shared bearer secret used when Mode=StaticToken (env / user-secrets only, never checked in) |
Mcp:Auth:Issuer / Audience |
the OAuth authorization server’s issuer + this server’s resource identifier (the token’s aud; mandatory under OAuth) |
Mcp:Auth:SigningKey |
lab-only: validate tokens against this symmetric HMAC key instead of discovering the issuer’s JWKS |
Mcp:Observability:Otlp:Endpoint, Mcp:Identity:* |
OTLP push + fleet identity; see Observability |
Fallen8Target:BaseUrl |
the Fallen-8 REST base URL this server bridges to (default http://localhost:8080) |
Fallen8Target:ApiKey / ApiKeyHeader |
the server’s single downstream credential (header default X-Api-Key) |
Fallen8Target:TlsInsecure |
lab-only: skip downstream TLS validation (loudly logged) |
Fallen8Target:TimeoutSeconds |
deadline on one bridged REST call (default 330, clamped to 1..2147483: a non-positive value is a one-second deadline rather than none, and a very large one is capped at what the transport can be armed with rather than throwing). Exceeding it returns the bridge’s retryable 504 “Fallen-8 timeout”. The default sits above the instance’s own longest budget on a bridged route (Fallen8:Embedding:TimeoutSeconds, 300s, which a semantic search spends embedding the query) so the instance’s error wins instead of being cut off here |
For contributors: engine → REST → MCP
Section titled “For contributors: engine → REST → MCP”The MCP surface must not fall behind the database. When a capability grows in the engine and
gains a REST endpoint, it must also be surfaced to agents as an MCP tool, or be a conscious,
reasoned deferral. This is enforced by a test (McpRestCoverageTest): a new REST endpoint that
is neither bridged nor deferred fails the build.