chartcoach
Reference

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 mcp

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

  1. describe reports catalog identity, table schemas, vocabulary, and profile names. Its mcp field reports the SQL engine, deadline, and active search configuration.
  2. sql selects compact guideline entry candidate IDs.
  3. search, when configured, finds candidates through the startup index profile.
  4. read returns complete selected entry records and source fields.
  5. cite returns 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 0

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

  • none returns an empty sources list.
  • minimal includes reference_id, authors_text, year, source_title, doi, and url.
  • full adds all parsed source fields and the entry's raw BibTeX bibliography references.

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: [].

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

Each 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

TransportClient connection
stdioLaunch the command directly
ssehttp://127.0.0.1:8000/sse
streamable-httphttp://127.0.0.1:8000/mcp
chartcoach mcp --transport streamable-http

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

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

The 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.com
uvx --from "chartcoach[mcp,tracing]" chartcoach mcp

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

FieldValue
Typeretriever for search, sql, and read. tool for describe and cite.
InputTool arguments as the client sent them
OutputStructured tool result, or the MCP SDK's error text
LevelERROR for failed calls, with the error message as the status message
VersionInstalled chartcoach version
Tagschartcoach and mcp

Observation metadata identifies the catalog, client, and failure:

KeyValue
catalogIdRelease digest, or a digest of the entries and manifest
catalogReleaseIdRelease digest, when the catalog was opened from a release
clientMCP clientInfo name, with its version in clientVersion
userAgentHTTP User-Agent header
errorCodeCatalog error code such as lookup, or tool_error
sessionSourceIdentifier 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:

sessionSourceSent byGroups
baggageClients that send langfuse_session_id baggage in _metaThe client's session
openai/sessionChatGPT, as a _meta field on each tool callOne conversation
CLAUDE_CODE_SESSION_IDClaude Code, as an environment variable for the stdio servers it startsOne Claude Code session
processOther stdio clientsOne server process
Mcp-Session-IdStateful HTTP clients (--stateful), stored as a truncated SHA-256One 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=c599adaf6ddca0ffa0b77a93ed7fa705

Configuration

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.

VariableControls
CHARTCOACH_SOURCECatalog path or URI
CHARTCOACH_INDEX_PROFILERelease-owned startup index profile
CHARTCOACH_EMBEDDING_VARSPrivate JSON registry-variable file
CHARTCOACH_MCP_TRANSPORTstdio, sse, or streamable-http
CHARTCOACH_MCP_HOSTHTTP listener address
CHARTCOACH_MCP_PORTHTTP listener port
CHARTCOACH_MCP_LOG_LEVELServer log level
CHARTCOACH_MCP_PUBLIC_URLPublic HTTP origin, without a path
CHARTCOACH_MCP_PATHStreamable HTTP endpoint
CHARTCOACH_MCP_ALLOWED_HOSTSComma-separated Host allowlist
CHARTCOACH_MCP_ALLOWED_ORIGINSComma-separated Origin allowlist
CHARTCOACH_MCP_TOKENShared bearer token
CHARTCOACH_MCP_STATELESSLegacy HTTP session policy
CHARTCOACH_MCP_JSON_RESPONSEJSON responses; false enables streaming
CHARTCOACH_MCP_SQL_TIMEOUTSQL execution deadline, in seconds
CHARTCOACH_MCP_TRACINGLangfuse 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.

On this page