MCP server
Describe, run SQL, read, cite, and search one Guideline Catalog through MCP.
The Model Context Protocol
(MCP) server resolves one Catalog at startup. Every tool uses that catalog
for the server lifetime.
Install and start
uv tool install "chartcoach[mcp]"
chartcoach mcpThe default standard-input and standard-output (stdio) transport is launched
by the MCP client and has no URL.
Discovery workflow
Start with describe, then choose candidates through SQL or configured search:
describereports catalog identity, table schemas, vocabulary, and profile names. Itsmcpfield reports the SQL engine, deadline, and active search configuration.sqlselects compact guideline entry candidate IDs.search, when configured, finds candidates through the startup index profile.readreturns complete selected entry records and source fields.citereturns the public guideline and source citations.
Enumerate all guidelines
Use sql to enumerate the catalog in a stable order:
select id, title, description
from guidelines
order by id
limit 100 offset 0Set the tool's limit to at least the SQL page size. Advance offset by
row_count and stop when a page returns fewer than 100 rows. Keep the projection
and ordering consistent. Pass discovered IDs to read, then cite the entries
used in the answer.
truncated reports the tool's row cap. A query with limit 100 can return
truncated: false while later pages still exist.
For a cheap result-delivery diagnostic, call sql with
statement="SELECT 1 AS probe" and limit=1. The returned rows must contain
{"probe": 1} in both the text and structured results.
Every tool advertises a success schema and an error branch. For example,
sql(statement="SELECT 1; SELECT 2") returns:
{
"error": {
"code": "invalid_input",
"message": "SQL queries must contain exactly one statement.",
"details": {},
"hints": [
"Pass one SELECT query. Multiple statements are rejected.",
"Use describe.tables for table and column names, or sql with DESCRIBE guidelines for native SQL types."
]
}
}Successful results and catalog failures return the same JSON object in
structuredContent and a text content block. Text content carries the full
data, including explicit empty rows or matches.
Catalog failures contain code, message, details, and MCP-specific hints,
with MCP isError: true. Recovery guidance names tools the client can call.
Invalid tool argument types and limits use the MCP SDK's text error response.
Stable catalog codes distinguish
lookup, invalid_input, integrity, unavailable_capability,
incompatible_profile, embedding_failure, and operation_failed.
operation_failed identifies failures during a native operation or resource
access.
Tools
describe
describe(profile=null) reports the bound catalog. Passing a profile
reads its small verified profile.json. The index stays unopened and the
embedding function stays unconstructed.
The result includes tables with names, row counts and columns, section_roles,
label_families, profiles, optional profile metadata, and catalog identity.
mcp contains sql_engine, sql_timeout_seconds, and search. When search is
configured, mcp.search reports its startup profile, filter_columns, and
document_roles. It is null when search is unconfigured.
The profiles list describes the catalog's available profiles. Inspecting one
with describe(profile=...) leaves the startup search profile unchanged.
sql
sql(statement, limit=100) accepts one read-only SELECT or a schema query such
as DESCRIBE guidelines. limit is a positive integer that caps returned rows.
The result contains typed columns, rows, truncation state, and catalog identity.
DuckDB is the SQL engine. describe reports
Polars dataframe column types. Use DESCRIBE guidelines
through sql for the native SQL types.
External file and network access is disabled for MCP queries. MCP interrupts SQL
execution after five seconds by default. Configure CHARTCOACH_MCP_SQL_TIMEOUT
to change the deadline.
read
read(ids, roles=null, source_detail="minimal") accepts a list of guideline
entry IDs and optional section roles. Copy guideline IDs from SQL rows or
search.matches[].id. Each record includes id, title, description,
labels, complete selected sections, and sources. Use role names from
describe.section_roles. null or [] retains all sections. Inspect context
and exceptions before applying advice.
An empty ID list returns records: [].
Source detail controls the source records:
nonereturns an emptysourceslist.minimalincludesreference_id,authors_text,year,source_title,doi, andurl.fulladds all parsed source fields and the entry's raw BibTeX bibliographyreferences.
Use cite for formatted citations. The result also contains entries_digest,
manifest_digest, and release_digest.
cite
cite(ids, url_template=...) accepts a list of guideline entry IDs. The
result contains guideline links, formatted source citations, and catalog
identity. An empty ID list returns records: [].
search
Set CATALOG_SOURCE to an indexed release and PROFILE to one of its profile
names, then start the server:
uv tool install "chartcoach[mcp,index]"
chartcoach mcp \
--source "$CATALOG_SOURCE" \
--profile "$PROFILE"search(text, limit=10, where=null, mode="fts") binds the startup index profile.
limit is a positive integer counting document hits before grouping them by
guideline. The result contains matches and match_count, the number of
returned guidelines. documents_considered counts the retained document hits.
documents_truncated indicates that more document hits exist beyond the limit.
Increase limit to consider more candidates. Full-text search (FTS) and hybrid
scores are relevance, with higher scores ranking first. Vector scores are
distances, with lower distances ranking first. Scores belong to one query and
profile. Match excerpts are bounded to 360 characters.
Each match includes a guideline id, title, description, labels,
matched_document_id, matched_role, matched_excerpt, excerpt_truncated,
score, and score_kind. The index splits each guideline into overview,
complete-document, and section rows whose parent_id is the guideline ID.
Several document hits can collapse to one guideline match, so the number of
matches can be smaller than limit. Pass matches[].id to read and cite.
Use read to obtain complete selected sections.
where accepts a LanceDB SQL expression. LanceDB
queries the catalog's text and vector index. Filters use the columns listed in
describe.mcp.search.filter_columns, with AND/OR and single-quoted string
literals. Filters are planned before semantic embedding. For example,
role = 'overview' searches entry overviews, while role = 'section.advice'
searches advice sections. Search document roles use the section. prefix.
The read tool uses the unprefixed section name such as advice.
Combine filters with catalog labels, for example
array_has(labels, 'basis:empirical') AND role = 'overview'.
Vector and hybrid search reconstruct the profile’s native LanceDB embedding
function. Variables such as $var:CHARTCOACH_EMBEDDING_API_KEY resolve from the startup
environment, including .env. Only names declared by the selected profile are
read. A credential field, such as api_key, falls back to
CHARTCOACH_EMBEDDING_API_KEY when the variable it names is unset, so one key
serves any release. The profile's python_requirements must be installed, in any
version. A private JSON variable file overrides the environment values:
chartcoach mcp \
--source "$CATALOG_SOURCE" \
--profile "$PROFILE" \
--embedding-vars ./embedding-vars.jsonEach server keeps its resolved credentials and lazy embedding function locally;
composed servers do not write credentials to the global LanceDB registry.
chartcoach excludes credential values from tool results, CLI errors, rendered
configuration-validation messages, configuration dumps, and release artifacts.
Pydantic’s detailed ValidationError.errors() and .json() APIs retain input
values; avoid logging those for credential configuration. LanceDB and embedding providers control their own
diagnostic logs.
Missing-variable errors list the required names. The server reads its variable
file once, so restart it after updating that file.
Transports
| Transport | Client connection |
|---|---|
stdio | Launch the command directly |
sse | http://127.0.0.1:8000/sse |
streamable-http | http://127.0.0.1:8000/mcp |
chartcoach mcp --transport streamable-httpHTTP apps expose GET /healthz without authentication. It returns 200 after
the ASGI lifespan starts, and 503 outside it. Streamable HTTP includes the SDK
startup lifespan; SSE keeps the SDK resource lifespan on each client connection. Set CHARTCOACH_MCP_TOKEN to require
Authorization: Bearer <token> on MCP requests. This shared token works with
clients that support custom headers. To compose OAuth or other SDK features,
register chartcoach tools on your own MCP SDK server.
Deploy with uvx
Install uv on the host, select a working directory, and copy the
example environment file
to .env. For a reverse proxy such as Coolify, configure:
CHARTCOACH_MCP_TRANSPORT=streamable-http
CHARTCOACH_MCP_HOST=0.0.0.0
CHARTCOACH_MCP_PORT=8000
CHARTCOACH_MCP_PUBLIC_URL=https://mcp.chartcoach.dev
# Supply a private deployment secret:
# CHARTCOACH_MCP_TOKEN=Run the package entry point:
uvx --from "chartcoach[mcp]" chartcoach mcpPin chartcoach==<version> in the --from requirement for a repeatable deployment.
Route mcp.chartcoach.dev to port 8000, use /healthz for health checks, and
connect clients to https://mcp.chartcoach.dev/mcp. The proxy must preserve the
public Host header and support streaming when stream responses are enabled.
CHARTCOACH_MCP_PUBLIC_URL adds the normalized host and origin to the SDK allowlists
(use an ASCII or punycode hostname for international domains);
binding to 0.0.0.0 keeps DNS rebinding protection enabled. Additional hosts
and origins can be explicitly listed. Origin validation is separate from CORS;
add CORS middleware in a composed ASGI app if a browser client needs it.
Install chartcoach[mcp,index] for full-text search, or
chartcoach[mcp,embedding-openai] for OpenAI-compatible semantic search.
Keep the catalog/cache on persistent storage (XDG_CACHE_HOME on Linux), and
let your process manager restart the service. Each process resolves the catalog
selection once; restart to pick up a promoted release. The chat app deploys
separately at chat.chartcoach.dev; see the
chat deployment guide.
Deploy with Docker
infra/mcp.Dockerfile builds the server from the locked workspace with
uv, including OpenAI-compatible
semantic search and tool-call tracing, and starts it on port
8000 over Streamable HTTP:
docker build -f infra/mcp.Dockerfile -t chartcoach-mcp .
docker run -p 8000:8000 \
-e CHARTCOACH_MCP_PUBLIC_URL=https://mcp.chartcoach.dev \
-e CHARTCOACH_INDEX_PROFILE=openrouter-text-embedding-3-large \
-e CHARTCOACH_EMBEDDING_API_KEY \
-v chartcoach-cache:/cache \
chartcoach-mcpThe image runs as an unprivileged user, keeps the catalog cache in /cache, and
checks /healthz. Other settings in Configuration apply as
environment variables. On Coolify, create an application from this repository with
the Dockerfile build pack and infra/mcp.Dockerfile, expose port 8000, and mount
a volume at /cache.
Trace tool calls
Langfuse records each tool
call as a trace with its arguments, result, latency, and errors. Install the
tracing extra and set the Langfuse project keys in the server environment:
CHARTCOACH_MCP_TRACING=true
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.comuvx --from "chartcoach[mcp,tracing]" chartcoach mcpStartup fails when tracing is enabled and either key is missing. Use your
project's region or self-hosted URL. LANGFUSE_TRACING_ENVIRONMENT labels the
environment, and LANGFUSE_RELEASE identifies a deployment. The Langfuse client
reads its other environment variables,
such as LANGFUSE_SAMPLE_RATE. The server flushes buffered traces when the
transport stops, including after SIGTERM.
Enabling tracing sends every tool argument and result to the Langfuse project, including search text and SQL statements from MCP clients. Treat the project as a destination for that client data.
Each trace is named after its tool, such as search, and holds one observation:
| Field | Value |
|---|---|
| Type | retriever for search, sql, and read. tool for describe and cite. |
| Input | Tool arguments as the client sent them |
| Output | Structured tool result, or the MCP SDK's error text |
| Level | ERROR for failed calls, with the error message as the status message |
| Version | Installed chartcoach version |
| Tags | chartcoach and mcp |
Observation metadata identifies the catalog, client, and failure:
| Key | Value |
|---|---|
catalogId | Release digest, or a digest of the entries and manifest |
catalogReleaseId | Release digest, when the catalog was opened from a release |
client | MCP clientInfo name, with its version in clientVersion |
userAgent | HTTP User-Agent header |
errorCode | Catalog error code such as lookup, or tool_error |
sessionSource | Identifier that set the session, from the session table |
Langfuse groups traces into a session when the client identifies its conversation. The server uses the first identifier it finds:
sessionSource | Sent by | Groups |
|---|---|---|
baggage | Clients that send langfuse_session_id baggage in _meta | The client's session |
openai/session | ChatGPT, as a _meta field on each tool call | One conversation |
CLAUDE_CODE_SESSION_ID | Claude Code, as an environment variable for the stdio servers it starts | One Claude Code session |
process | Other stdio clients | One server process |
Mcp-Session-Id | Stateful HTTP clients (--stateful), stored as a truncated SHA-256 | One MCP connection |
The Langfuse user comes from langfuse_user_id baggage or ChatGPT's
openai/subject. Stateless HTTP calls that carry neither identifier become
individual traces. Filter them by client or userAgent.
Clients that trace their own work can join the tool observation to it. Send a
W3C traceparent and OpenTelemetry
baggage in the request _meta:
{
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
"baggage": "langfuse_session_id=run-42,langfuse_user_id=analyst-7"
}The tool observation becomes a child of the caller's span, in session run-42
for user analyst-7. These identifiers are client-reported labels for analysis.
Every tool call also writes one chartcoach.mcp log record with the tool,
outcome, duration, and trace ID. Tool arguments stay in the trace:
tool=read outcome=lookup duration_ms=15.8 trace_id=c599adaf6ddca0ffa0b77a93ed7fa705Configuration
The CLI reads .env from its working directory, without scanning parent
folders. --env-file PATH or CHARTCOACH_ENV_FILE selects another UTF-8 dotenv
file. Relative catalog and variable paths resolve from the working directory.
Precedence is command options, explicitly selected env files, inherited
environment, automatic .env files, then
schema defaults. Embedding JSON values override the corresponding environment
values. Files and credentials are read once at startup; restart after changing
them. Tokens are configured through the environment rather than command arguments.
Use chartcoach mcp --help for option defaults. The launch schema is the
source of truth for validation.
| Variable | Controls |
|---|---|
CHARTCOACH_SOURCE | Catalog path or URI |
CHARTCOACH_INDEX_PROFILE | Release-owned startup index profile |
CHARTCOACH_EMBEDDING_VARS | Private JSON registry-variable file |
CHARTCOACH_MCP_TRANSPORT | stdio, sse, or streamable-http |
CHARTCOACH_MCP_HOST | HTTP listener address |
CHARTCOACH_MCP_PORT | HTTP listener port |
CHARTCOACH_MCP_LOG_LEVEL | Server log level |
CHARTCOACH_MCP_PUBLIC_URL | Public HTTP origin, without a path |
CHARTCOACH_MCP_PATH | Streamable HTTP endpoint |
CHARTCOACH_MCP_ALLOWED_HOSTS | Comma-separated Host allowlist |
CHARTCOACH_MCP_ALLOWED_ORIGINS | Comma-separated Origin allowlist |
CHARTCOACH_MCP_TOKEN | Shared bearer token |
CHARTCOACH_MCP_STATELESS | Legacy HTTP session policy |
CHARTCOACH_MCP_JSON_RESPONSE | JSON responses; false enables streaming |
CHARTCOACH_MCP_SQL_TIMEOUT | SQL execution deadline, in seconds |
CHARTCOACH_MCP_TRACING | Langfuse tool-call tracing |
Repeated --allowed-host and --allowed-origin options replace the corresponding
environment allowlist. The public URL is always added. Modern MCP uses per-request
transport; the stateless setting controls legacy HTTP clients.
Compose in Python
Open the catalog yourself, add tools, and serve the ASGI app with your own host:
from chartcoach import open_catalog
from chartcoach.mcp import MCPConfig, create_app, create_server
catalog = open_catalog("./catalog/releases/<digest>")
server = create_server(catalog, sql_timeout=3.0)
@server.tool()
def ping() -> str:
return "pong"
app = create_app(server, config=MCPConfig(
transport="streamable-http",
public_url="https://mcp.chartcoach.dev",
))register_tools(server, catalog=catalog, profile=..., embedding_variables=...)
also attaches the tools to an existing mcp.server.MCPServer. create_server
and register_tools snapshot profile-declared environment variables by default;
pass embedding_environment={} to disable environment lookup, or an explicit
mapping to isolate it. Explicit embedding_variables take precedence.
load_config(environment=..., overrides=...) resolves the same launch schema
without reading dotenv or mutating process state. MCPConfig reads a selected
embedding JSON file once during validation, including direct Python construction. run(config) owns catalog
opening and the selected server loop. For an embedded launch, load dotenv in
your application before load_config() if needed.
create_server adds the attributes from Trace tool calls
to the MCP SDK's OpenTelemetry
tool spans and logs each call. Any tracer provider configured in your process
exports those spans, including the one a langfuse.Langfuse() client registers.
run(config) starts Langfuse when config.tracing is true and flushes it when the
transport stops.
The returned ASGI app owns the MCP SDK lifespan. If you mount it in another
Starlette or FastAPI app, enter app.router.lifespan_context(app) from the
parent lifespan. Keep synchronous catalog/provider work in the SDK’s worker
threads. Importing the base chartcoach package never loads MCP, LanceDB, or a
provider SDK.
Configure embedding models
The model connection guide owns the shared text and embedding environment contract and profile-build workflow. It covers independent OpenAI-compatible endpoints, dedicated credentials, custom key variable names, and selecting the resulting profile in MCP and chat. Runtime model identity comes from the catalog profile; credentials stay deployment-owned.