Files
sure/docs/hosting/mcp.md
T
Brandon 25a9011f14 feat(ai): analytical tool-set upgrade for the builtin assistant (#3064)
* fix(assistant): survive tool failures with error and hint results instead of aborting the turn

A tool exception used to raise FunctionExecutionError out of the responder
loop, turning the whole turn into a generic chat error banner. An unknown
tool name was worse: the rescue block itself crashed (fn.name on nil).

Tool failures now come back to the model as data ({error, hint}) so the
conversation survives and the model can retry once with corrected
arguments. The catch-all branch logs and tells the model not to retry.
FunctionExecutionError remains defined for API compatibility.

* fix(assistant): strict schemas declare every property as required

get_categories and get_tags declared an optional page property while
inheriting strict mode, which is invalid under strict function calling
(every property must be listed in required). Both now opt out of strict
mode like every other paginated tool, and gain a page_size param
(1..100) while being touched.

A registry-walking test asserts the invariant for every current and
future tool, preview tools included.

* fix(assistant): HistoryTrimmer always keeps the newest turn

Trimming iterates newest-first and stopped at the first group over
budget. When the newest group alone exceeded the budget, everything was
dropped, including the user message the model was being asked to
answer, and the provider received only the system prompt. The newest
group now always survives.

* perf(assistant): compact AI time series and make account history opt-in

get_accounts shipped a 5-year monthly series for every account on every
call, roughly 60 formatted money strings per account, which dominates
the tool payload for multi-account families and swamps small
self-hosted context windows. The series is now opt-in
(include_balance_series) and bounded by a named period (series_period,
default last_365_days).

to_ai_time_series states the currency once and emits numeric values
instead of formatting every point; the system prompt already tells the
model how to render currency.

get_accounts also now returns account ids (they are what other tools
accept as account_ids filters) and respects the visible scope, so
hidden accounts no longer leak into responses.

* refactor(assistant): id and name filters replace user-data enums in get_transactions

The schema inlined every account, category, merchant, and tag name as
enum values on every request. That grows without bound with family
data, defeats provider prompt caching (definitions change whenever a
name does), and is the pattern that made empty-enum pruning necessary
in the first place.

Filters are now plain string arrays documented as exact names from the
sibling get_* tools, which Transaction::Search already resolves
server-side, plus an account_ids UUID filter. New params: page_size
(1..100), sort_by amount, types (income/expense/transfer, the way to
exclude transfers), and statuses (pending/confirmed).

The three now-unused enum helpers are removed from the base class;
family_tag_names stays for update_tag, which still identifies tags by
name.

* feat(assistant): add get_merchants and get_recurring_transactions

Merchants were unreachable: names appeared nowhere and
update_transaction's merchant_id had no source of ids, making it
unusable. get_merchants lists id, exact name, and source, scoped
through available_merchants_for so merchants seen only in accounts
hidden from the user never leak.

Recurring transactions had a model, an Upcoming view, and no assistant
reach. get_recurring_transactions lists detected and manual recurring
items (status filter defaulting to active, optional
upcoming_within_days window) with per-currency totals of active
non-transfer items, answering subscription and upcoming-bill questions
directly instead of via transaction paging.

* feat(assistant): flexible periods on get_balance_sheet and trends on get_income_statement

get_balance_sheet was hard-wired to five years of monthly history with
no parameters, although Period supports arbitrary ranges and the chart
builder takes any interval. It now accepts a named period or custom
dates plus an interval, with a 400-point cap so a day-granularity
request over a decade returns an error instead of a giant series. The
default call is byte-compatible with the old shape. The balance sheet
object is also memoized; it was being constructed four times per call.

get_income_statement gains the analysis surface the assistant lacked:
group_by month for a monthly income/expenses/net series (capped at 36
buckets), compare_previous_period for an equal-length prior window
with absolute and percent deltas, and account_ids to scope totals to
specific accounts via IncomeStatement#totals_for. Category breakdowns
are family-wide by construction, so the account-filtered view omits
them and says why. Unknown or inaccessible account ids come back as a
soft failure naming the ids so the model can correct itself.

* feat(assistant): preview reads for insights and valuations

The Insights feed is generated nightly with pre-computed numbers, and
the chat assistant could not read a word of it. get_insights returns
the visible feed (type filter, acknowledged toggle, limit) without
marking anything read; an assistant read is not the user viewing the
feed. It sits in PREVIEW_FUNCTION_CLASSES because the feature itself is
preview-gated, which also keeps it off the default /mcp surface.

record_valuation was write-only: an agent recording provenance-cited
valuations had no way to audit what it wrote or find dates already
carrying a value. get_valuations lists valuation entries newest first
with kind and the citation notes, scoped to accessible visible
accounts.

* feat(assistant): cache-stable system prompt with session context

The prompt interpolated currency mid-text and the date near the end, so
no two requests shared a cacheable prefix, and it told the model
nothing about the family: not one account name, not a single category.
Models opened most conversations blind, either wandering through tools
or answering without data.

The prompt is now STATIC_INSTRUCTIONS, a frozen constant that is
byte-identical for every request (providers discount an
exactly-repeated prefix; tool definitions are also stable now that
schemas carry no user data), followed by a trailing Session context
block holding everything volatile: date, date format, currency details,
an account roster with balances, and category names.

The static half gains a request-classification rule (CHAT / LOOKUP /
ANALYSIS), a reuse-what-you-have rule with an explicit re-fetch
carve-out, specific-tool preference, and the error/hint retry-once
rule that pairs with the tool soft-fail contract.

Context stays cheap by construction: the roster collapses to per-type
counts beyond 25 accounts, categories to a count beyond 60 names, and
both collapse whenever the configured context window is under 4096
(the self-hosted default is 2048), via the new Assistant::TokenBudget
helper. Intro chats are untouched.

* feat(assistant): raise tool-round cap to 8 with a no-tools grace turn; instructions-aware history budget

Five rounds was tight for a tool surface that now supports real
analysis chains, and hitting the cap raised ToolCallLimitError, which
surfaced to the user as a dead chat with an error banner. The default
is now eight rounds (env override unchanged), and on the final
permitted round the follow-up request offers no tools, so the model
must answer in text with whatever it gathered. The limit error remains
as a defensive backstop.

The generic-path history budget reserved a flat 256 tokens for a
system prompt that already estimates well past that; the trimmer now
budgets against the actual instructions when available.

LLM_MAX_RESPONSE_TOKENS was reserved in budget math but never sent to
the provider. It is now sent (max_tokens on chat completions,
max_output_tokens on the Responses API) only when explicitly
configured via ENV or a stored Setting; stock installs keep today's
uncapped behavior.

* test(evals): chat golden v2 exercising the real prompt and registry

The eval runner scored a fiction: hardcoded instructions and four fake
permissive tool schemas, so a prompt or registry regression could
sail through green. It now runs STATIC_INSTRUCTIONS plus a fixed
synthetic session context and builds definitions from
Assistant.function_classes against a reference user (classes whose
schema cannot build are skipped with a log line, never faked).

chat_golden_v2 adds routing scenarios the upgrade cares about: CHAT
classification must use no tools, aggregates route to
get_income_statement / get_balance_sheet rather than transaction
paging, and the new analytical tools are selected with sensible
params. The dataset header documents the harness's single-shot
limitation.

* docs(ai,mcp): current tool tables, responder loop, prompt structure, timeout math

Both docs listed 7 tools against a registry of 19, in three separate
drift-prone copies. mcp.md now carries the canonical tables (default +
preview); ai.md links to them from the MCP section, keeps one grouped
functions list for the architecture chapter, and replaces its stale
hardcoded registry snippet with a pointer to assistant.rb.

The architecture section gains the contracts contributors need when
adding a function: the responder loop (rounds vs calls, cap 8, the
no-tools grace turn) and the error/hint soft-failure convention, plus
the prompt's static/session-context split and its collapse gates.
Timeout guidance is recomputed for the new default cap.

* fix(assistant): address automated review findings

Codex and CodeRabbit findings on the initial push, all verified before
changing anything:

- AI time series rounded every value to two decimals, which turns
  0.001 BTC into 0.0; values now round to the currency's own precision
  (BTC 8, CLF 4, OMR 3).
- get_income_statement validated account_ids against all visible
  accounts, but totals_for excludes hidden, excluded-from-reports and
  tax-advantaged accounts, so those ids produced silent zeros. Ids now
  validate against income_statement.eligible_accounts and the soft
  failure explains eligibility.
- get_recurring_transactions computed totals from the displayed rows,
  so past the 200-row cap the value labeled a total was partial. Totals
  now aggregate over the full filtered scope in SQL, and the response
  carries total_results and a truncated flag. The upcoming_within_days
  window also starts at today, matching its documentation; overdue
  items appear in unwindowed calls.
- get_valuations silently dropped a malformed date filter and presented
  unfiltered data as filtered; malformed dates now return invalid_date.
- get_balance_sheet returned a generic failure for a reversed custom
  range because Period's own validation raises past the Date::Error
  rescue; it now returns the structured invalid_date error.
- get_insights documents that its family-wide scope matches the web
  feed exactly (InsightsController serves Current.family.insights to
  every member), so the tool exposes nothing the /insights page does
  not already show the same user.
- Tests: limit clamp proven against more insights than the cap,
  Setting fallbacks stubbed in the provider budget tests, currency
  precision and reversed-range regression tests added.

* refactor(assistant): apply reviewer nitpicks

- order declares type alongside its enum, matching sort_by
- page-size clamp deduplicated into the base class (MAX_PAGE_SIZE +
  shared resolved_page_size); dead per-tool copies removed
- get_accounts preloads balance rows only when the series is requested
- get_income_statement validates the bucket count before running any
  aggregation work

Deliberately unchanged: the balance sheet's monthly_history key. The
default response shape stays byte-compatible for existing MCP
consumers, and the nested series already states its interval.

* fix(assistant): second-round review findings on get_valuations

- A reversed date range (start after end) now returns the structured
  invalid_date error instead of presenting an empty result as filtered
  data, matching get_balance_sheet's handling.
- Page numbers are normalized before pagination: Pagy raises on zero,
  negative or non-numeric pages. The fix lands as a shared
  resolved_page helper on the base class and applies to every
  paginated tool (categories, tags, merchants, transactions, holdings,
  valuations), since all shared the same page-or-1 pattern; schemas
  declare minimum: 1.

* fix(assistant): round series amounts as BigDecimal before Float conversion

Converting to Float first can perturb the value at the requested
precision; round the exact decimal, then convert for JSON.

* fix(assistant): address maintainer review findings

- get_accounts no longer fails the whole listing when one account's
  start date lies beyond the requested period (start_date derives from
  the first entry, which can be future-dated); that account simply has
  no series. The unrescued Period.custom was reachable exactly there.
- The balances preload is gone: the series goes through
  Balance::ChartSeriesBuilder, which runs its own query keyed by
  account ids, so the eager-loaded rows were loaded and discarded.
- Provider::Openai#context_window now delegates to
  Assistant::TokenBudget, removing the duplicated ENV > Setting >
  default precedence so prompt assembly and the provider can never
  disagree about the window.

* fix(ai): final no-tools round uses tool_choice none instead of dropping tools

Anthropic rejects requests whose messages contain tool_use blocks when
no tools are defined, so re-requesting with an empty tool list made the
final-round grace die in a provider 400 on Anthropic models. The final
round now sends the real tool definitions with tool_choice none, which
both providers accept, and the model answers in prose as intended.

* fix(assistant): scope every income statement read to the requesting user

get_income_statement validated account_ids against the user-scoped statement
but computed every total from an unscoped one. IncomeStatement falls back to
Current.user, which is nil in the assistant job and the MCP endpoint, so the
unscoped reads dropped the included_in_finances_for filter and reported
family-wide totals next to ids that had been checked against a narrower set.

Route all reads through one memoized user-scoped statement, the idiom
get_balance_sheet already uses. Also lets the per-instance memoization in
IncomeStatement apply across the eligibility check and the totals.

Adds a regression test that fails without the change, plus a companion test
asserting eligibility and totals agree on scope. Guard the strictness walk
against an empty registry so it cannot silently assert nothing.
2026-08-21 21:58:47 +02:00

428 lines
15 KiB
Markdown

# MCP Server for External AI Assistants
Sure includes a Model Context Protocol (MCP) server endpoint that allows external AI assistants like Claude.ai, Claude Desktop, GPT agents, or custom AI clients to query and act on your financial data.
## What is MCP?
[Model Context Protocol](https://modelcontextprotocol.io/) is a JSON-RPC 2.0 protocol that enables AI assistants to access structured data and tools from external applications. Instead of copying and pasting financial data into a chat window, your AI assistant can directly query Sure's data through a secure API.
This is useful when:
- You want to use an external AI assistant (Claude, GPT, custom agents) to analyze your Sure financial data
- You prefer to keep your LLM provider separate from Sure
- You're building custom AI agents that need access to financial tools
## Authentication Modes
Sure supports two ways to authenticate MCP clients:
### 1. OAuth 2.0 / dynamic client registration (recommended)
This is the best option for Claude.ai and other MCP clients that support OAuth. Sure exposes:
- `/.well-known/oauth-protected-resource`
- `/.well-known/oauth-authorization-server`
- `POST /register` for dynamic client registration
These endpoints let compatible MCP clients register a public OAuth client, redirect you back to Sure for sign-in, and receive a bearer token with the `read_write` scope.
### 2. Static bearer token via environment variables
This is the simpler fallback for custom agents, scripts, and deployments where you want to pin the MCP server to a specific Sure user.
Set these environment variables:
| Variable | Description | Example |
|----------|-------------|---------|
| `MCP_API_TOKEN` | Bearer token for authentication | `your-secret-token-here` |
| `MCP_USER_EMAIL` | Email of the Sure user whose data the assistant can access | `user@example.com` |
Both variables are required for the legacy token flow. OAuth clients using the
MCP discovery and dynamic registration endpoints do not need these variables.
### Generating a secure token
Generate a random token for `MCP_API_TOKEN`:
```bash
# macOS/Linux
openssl rand -base64 32
# Or use any secure password generator
```
### Choosing the user for static-token auth
The `MCP_USER_EMAIL` must match an existing Sure user's email address. The AI assistant will have access to all financial data for that user's family.
> [!CAUTION]
> The AI assistant can call the MCP tools available to the specified user. This includes reading financial data and write-capable tools such as statement import, goal/category/tag changes, transaction updates, and budget updates. Only set this for users you trust with your AI provider.
## Configuration
### Docker Compose
Add the environment variables to your `compose.yml`:
```yaml
x-rails-env: &rails_env
MCP_API_TOKEN: your-secret-token-here
MCP_USER_EMAIL: user@example.com
```
Both `web` and `worker` services inherit this configuration.
### Kubernetes (Helm)
Add the variables to your `values.yaml` or set them via Secrets:
```yaml
env:
MCP_API_TOKEN: your-secret-token-here
MCP_USER_EMAIL: user@example.com
```
Or create a Secret and reference it:
```yaml
envFrom:
- secretRef:
name: sure-mcp-credentials
```
## Protocol Details
The MCP endpoint is available at:
```
POST /mcp
```
### Authentication
MCP supports OAuth authorization-code flow for clients such as Claude Code.
Clients should discover the protected-resource metadata, register dynamically,
request the advertised `read_write` scope, and send the resulting access token
as a Bearer token. Dynamically registered clients are assigned this scope so
their tokens can authenticate to MCP.
For self-hosted deployments or clients without OAuth support, requests may use
the legacy `MCP_API_TOKEN` as a Bearer token:
```
Authorization: Bearer <token>
```
That token can come from either:
- an OAuth authorization flow handled by the MCP client, or
- the static `MCP_API_TOKEN` environment variable described above.
### Supported Methods
Sure implements the following JSON-RPC 2.0 methods:
| Method | Description |
|--------|-------------|
| `initialize` | Protocol handshake, returns server info and capabilities |
| `tools/list` | Lists available financial tools with schemas |
| `tools/call` | Executes a tool with provided arguments |
### Available Tools
The MCP endpoint exposes the same tool registry used by Sure's built-in assistant. Clients should treat `tools/list` as the source of truth.
At the time of writing, `tools/list` includes:
| Tool | Description |
|------|-------------|
| `get_transactions` | Search transactions with filters (exact names or ids), sorting by date or absolute amount, and pagination |
| `get_recurring_transactions` | Detected and manual recurring transactions (subscriptions, bills, salaries) with expected dates and per-currency totals |
| `get_accounts` | Accounts with ids and current balances; pass `include_balance_series: true` for a period-bounded history series |
| `get_holdings` | Query investment holdings |
| `get_balance_sheet` | Net worth, assets and liabilities with a configurable history period and interval |
| `get_income_statement` | Income and expenses for a period, with optional monthly series, prior-period comparison and account filtering |
| `get_budget` | Budget summary for a month, with optional prior months |
| `get_merchants` | Merchants with the ids `update_transaction` accepts and the exact names `get_transactions` filters on |
| `get_tags` | Tags with pagination |
| `get_categories` | Categories with hierarchy and pagination |
| `create_goal` | Create a savings goal linked to depository accounts |
| `create_tag` / `update_tag` | Manage tags |
| `create_category` / `update_category` | Manage categories |
| `update_transaction` | Edit a transaction's metadata (name, notes, category, merchant, tags) |
| `update_budget` | Update budget allocations for a month |
| `import_bank_statement` | Import bank statement data |
| `search_family_files` | Search documents uploaded through the import flow. Note this is the vector-store document index, not the Statement Vault — statements archived via `upload_account_statement` are not searchable through it |
These are the same tools used by Sure's built-in AI assistant.
### Preview Tools
These additional tools appear only when the MCP user has opted into preview
features (Settings → Preferences). Until then they are absent from `tools/list`,
and calling one by name returns an "Unknown tool" error. The Statement Vault
tools additionally require the user to be an admin or member, matching the
permissions enforced in the web UI.
| Tool | Description |
|------|-------------|
| `upload_account_statement` | Store a statement document (PDF/CSV/XLSX) in the Statement Vault; deduplicates by SHA-256 |
| `list_account_statements` | List vault documents with their SHA-256, period, linked account and review status |
| `get_account_statement` | One statement's details and its reconciliation checks against the ledger — present only once someone has entered the statement's opening/closing balances in the web UI, since nothing extracts them from the document. Does not return the file: stored documents are served only to a signed-in browser session |
| `get_statement_coverage` | Month-by-month statement coverage for an account: `covered`, `missing`, `mismatched`, `ambiguous`, `duplicate`, `not_expected`, each with a reconciliation status |
| `record_valuation` | Record an account's value on a date, with a required source citation |
| `get_valuations` | List recorded valuations newest first, including the citation stored in each entry's notes; the read pair for `record_valuation` |
| `get_insights` | Read the proactive insights feed (spending anomalies, cash-flow warnings, subscription audits and more) without marking anything read |
They exist for agents that maintain a document-backed record of a family's
wealth over time. See
[Wealth history with an external agent harness](../llm-guides/wealth-agent-harness.md).
## Example Requests
### Initialize
Handshake to verify protocol version and capabilities:
```bash
curl -X POST https://your-sure-instance/mcp \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize"
}'
```
Response:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "sure",
"version": "1.0"
}
}
}
```
### List Tools
Get available tools with their schemas:
```bash
curl -X POST https://your-sure-instance/mcp \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}'
```
Response includes tool names, descriptions, and JSON schemas for parameters.
### OAuth discovery
MCP clients that support OAuth can discover Sure's metadata automatically:
```bash
curl https://your-sure-instance/.well-known/oauth-protected-resource
curl https://your-sure-instance/.well-known/oauth-authorization-server
```
The authorization-server metadata includes:
- `authorization_endpoint`: `https://your-sure-instance/oauth/authorize`
- `token_endpoint`: `https://your-sure-instance/oauth/token`
- `registration_endpoint`: `https://your-sure-instance/register`
- `scopes_supported`: `["read_write"]`
### Call a Tool
Execute a tool to get transactions:
```bash
curl -X POST https://your-sure-instance/mcp \
-H "Authorization: Bearer your-secret-token" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_transactions",
"arguments": {
"start_date": "2024-01-01",
"end_date": "2024-01-31"
}
}
}'
```
Response:
```json
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "[{\"id\":\"...\",\"amount\":-45.99,\"date\":\"2024-01-15\",\"name\":\"Coffee Shop\"}]"
}
]
}
}
```
## Security Considerations
### Transient Session Isolation
The MCP controller creates a **transient session** for each request. This prevents session state leaks that could expose other users' data if the Sure instance is using impersonation features.
Each MCP request:
1. Authenticates the token
2. Loads the authorized Sure user
3. Creates a temporary session scoped to that user
4. Executes the tool call
5. Discards the session
This ensures the AI assistant can only access data for the intended user.
### Pipelock Security Scanning
For production deployments, we recommend using [Pipelock](https://github.com/luckyPipewrench/pipelock) to scan MCP traffic for security threats.
Pipelock provides:
- **DLP scanning**: Detects secrets being exfiltrated through tool calls
- **Prompt injection detection**: Identifies attempts to manipulate the AI
- **Tool poisoning detection**: Prevents malicious tool call sequences
- **Policy enforcement**: Block or warn on suspicious patterns
- **Signed receipts**: Produces verifiable evidence for mediated MCP decisions when the flight recorder is configured with storage and a signing key
See the [Pipelock documentation](pipelock.md) and the example configuration in `compose.example.ai.yml` for setup instructions.
### Network Security
The `/mcp` endpoint is exposed on the same port as the web UI (default 3000). For hardened deployments:
**Docker Compose:**
- The MCP endpoint is protected by the `MCP_API_TOKEN` but is reachable on port 3000
- For additional security, use Pipelock's MCP reverse proxy (port 8889) which adds scanning
- See `compose.example.ai.yml` for a Pipelock configuration
**Kubernetes:**
- Use NetworkPolicies to restrict access to the MCP endpoint
- Route external agents through Pipelock's MCP reverse proxy
- See the [Helm chart documentation](../../charts/sure/README.md) for Pipelock ingress setup
## Production Deployment
For a production-ready setup with security scanning:
1. **Download the example configuration:**
```bash
curl -o compose.ai.yml https://raw.githubusercontent.com/we-promise/sure/main/compose.example.ai.yml
curl -o pipelock.example.yaml https://raw.githubusercontent.com/we-promise/sure/main/pipelock.example.yaml
```
2. **Set your MCP credentials in `.env`:**
```bash
MCP_API_TOKEN=your-secret-token
MCP_USER_EMAIL=user@example.com
```
3. **Start the stack:**
```bash
docker compose -f compose.ai.yml up -d
```
4. **Connect your AI assistant to the Pipelock MCP proxy:**
```
http://your-server:8889
```
The Pipelock proxy (port 8889) scans all MCP traffic before forwarding to Sure's `/mcp` endpoint.
## Connecting AI Assistants
### Claude.ai
Sure's Settings UI is already geared toward Claude.ai OAuth integrations:
1. Open **Settings -> Integrations** in Claude.ai
2. Click **Add integration**
3. Paste your Sure MCP URL
4. Claude redirects you to Sure to sign in and authorize access
If you are using Pipelock, use the reverse-proxy URL on port `8889`. Otherwise use the app URL ending in `/mcp`.
### Claude Desktop
If your Claude Desktop build expects a raw MCP endpoint instead of an OAuth integration flow, point it at:
- `http://your-server:8889` when using Pipelock, or
- `http://your-server:3000/mcp` for direct access
Use either the client's OAuth support or a bearer token, depending on what that build supports.
### Custom Agents
Any AI agent that supports JSON-RPC 2.0 can connect to the MCP endpoint. The agent should:
1. Send a POST request to `/mcp`
2. Include the `Authorization: Bearer <token>` header
3. Use the JSON-RPC 2.0 format for requests
4. Handle the protocol methods: `initialize`, `tools/list`, `tools/call`
## Troubleshooting
### "unauthorized" error
**Symptom:** Requests return HTTP 401 with "unauthorized"
**Fix:** Verify one of these is true:
- The OAuth flow completed successfully and the client is sending the issued bearer token
- The static token matches `MCP_API_TOKEN`
- If you are using the static-token flow, `MCP_USER_EMAIL` matches an existing Sure user
### Static token works, but the user still gets rejected
**Symptom:** Requests return HTTP 401 even though the bearer token matches `MCP_API_TOKEN`
**Fix:** The `MCP_USER_EMAIL` probably does not match an existing user. Check that:
- The email is correct
- The user exists in the database
- There are no typos or extra spaces
### Pipelock connection refused
**Symptom:** AI assistant cannot connect to Pipelock's MCP proxy (port 8889)
**Fix:**
1. Verify Pipelock is running: `docker compose ps pipelock`
2. Check Pipelock health: `docker compose exec pipelock /pipelock healthcheck --addr 127.0.0.1:8888`
3. Verify the port is exposed in your `compose.yml`
## See Also
- [External AI Assistant Configuration](ai.md#external-ai-assistant) - Configure Sure's chat to use an external agent
- [Pipelock Security Proxy](pipelock.md) - Set up security scanning for MCP traffic
- [Model Context Protocol Specification](https://modelcontextprotocol.io/) - Official MCP documentation