Skip to content

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.

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:up

F8_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-mcp

For 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 -- --stdio

As 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 --stdio

It 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.

Claude Code, over Streamable HTTP. The default dev server is anonymous:

claude mcp add --transport http fallen8 http://localhost:8090

When 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-plugin
claude plugin install fallen-8@fallen-8

It 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.

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.

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 (delete on f8_plugins and f8_storedquery; ingest_text/delete/bind on f8_documents), which appear in those tools’ op list 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 on f8_paths/f8_subgraph (including a fragment inside a patterns step), the register op on f8_storedquery, and the register_algorithm/register_function ops on f8_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.

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 with F8_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:code each 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.

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).

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.

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.

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

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.