MCP
One URL and a key.
FormulaSignal runs a remote MCP server on the Streamable HTTP transport at https://formulasignal.com/mcp. There is nothing to install, no package to trust, and no code of ours running on your machine. 18 tools, the same 18 capabilities the REST API exposes, with the same key and the same limits.
- Set it up in your client
- Any other remote-MCP client
- stdio-only clients
- Verify by hand
- What the agent gets
- Instructing your agent
Set it up in your client
Three clients are written out below because they take a remote MCP server and a custom header directly, which is what this endpoint needs. Each block is the shape that client’s own current documentation specifies. What was checked for each is stated with it, because “supported” is a word that costs somebody an afternoon when it turns out to mean “probably”.
Claude Code
One command, and it accepts the header directly.
CLAUDE CODE
claude mcp add --transport http formulasignal \
https://formulasignal.com/mcp \
--header "Authorization: Bearer $FORMULASIGNAL_API_KEY"Then claude mcp list to confirm it connected, and ask it something: Resolve C4 Original and tell me its current caffeine amount with evidence. To commit the server to a repository instead, put it in .mcp.json. The type field is required whenever there is a url; without it the entry is read as a stdio server.
.mcp.json
{
"mcpServers": {
"formulasignal": {
"type": "http",
"url": "https://formulasignal.com/mcp",
"headers": {
"Authorization": "Bearer fsk_live_..."
}
}
}
}Verified end to end against the live endpoint: added, connected, tools listed, and a Record tool and a Signal tool both called.
Cursor
~/.cursor/mcp.json for every project, or .cursor/mcp.json in one repository. Cursor interpolates ${env:NAME}, so the key can stay in your environment rather than in the file.
~/.cursor/mcp.json
{
"mcpServers": {
"formulasignal": {
"url": "https://formulasignal.com/mcp",
"headers": {
"Authorization": "Bearer ${env:FORMULASIGNAL_API_KEY}"
}
}
}
}VS Code
.vscode/mcp.json, and note the top-level key is servers rather than mcpServers. An input makes VS Code prompt for the key once and keep it out of the file entirely.
.vscode/mcp.json
{
"inputs": [
{
"type": "promptString",
"id": "formulasignal-key",
"description": "FormulaSignal API key",
"password": true
}
],
"servers": {
"formulasignal": {
"type": "http",
"url": "https://formulasignal.com/mcp",
"headers": {
"Authorization": "Bearer ${input:formulasignal-key}"
}
}
}
}For Cursor and VS Code the configuration above is the shape each client’s current documentation specifies, and the exact request it produces was replayed against the live endpoint: handshake, tool list and tool call all answered. The editors themselves were not driven by hand, so if a version of either one changes its config format, that is the part to check first.
Claude Desktop, and why it is not in the list
A custom connector in Claude Desktop takes a URL and, under advanced settings, an OAuth client id and secret. There is no field for an arbitrary header, and FormulaSignal authenticates with a bearer key rather than running an OAuth server, so a custom connector cannot carry a FormulaSignal key today. Use the stdio bridge below, which does work, or one of the three clients above. This is written down rather than left out because discovering it yourself takes twenty minutes.
Any other client that speaks remote MCP
The three above are the ones that were checked. Any other client supporting the Streamable HTTP transport with custom headers takes the same two values, whatever it calls the fields.
TYPICAL SHAPE
{
"mcpServers": {
"formulasignal": {
"type": "http",
"url": "https://formulasignal.com/mcp",
"headers": {
"Authorization": "Bearer fsk_live_..."
}
}
}
}If your client rejects Authorization, send the key as X-FormulaSignal-Key with no Bearer prefix. Both headers are checked identically. That alternative exists because an MCP client config is a JSON file a person edits by hand, and “remember to write Bearer in front of it” is a support ticket waiting to happen.
Clients that only speak stdio
Most MCP clients are still stdio only. mcp-remote is the community bridge for exactly this and it is not ours: it is a published npm package that proxies a local stdio server to a remote HTTP one.
{
"mcpServers": {
"formulasignal": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://formulasignal.com/mcp",
"--header",
"Authorization:${FS_AUTH}"
],
"env": {
"FS_AUTH": "Bearer fsk_live_..."
}
}
}
}The colon has no space after it and the space lives in the environment variable. That is a real quirk of how arguments are split, not a typo. If you would rather keep the key out of the process list, where any other user on the machine can read it, mcp-remote also takes --header-file with one Name: value per line.
mcp-remote is a third-party package and not ours. It is named because it is the bridge the MCP ecosystem actually uses, and because the alternative is telling people with a stdio-only client that they cannot connect at all.
Verify it by hand
The endpoint is ordinary HTTP, so you never have to guess whether the problem is your client or us. This is the whole handshake.
LIST THE TOOLS
curl -s https://formulasignal.com/mcp \
-H "Authorization: Bearer $FORMULASIGNAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'CALL ONE
curl -s https://formulasignal.com/mcp \
-H "Authorization: Bearer $FORMULASIGNAL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"formulasignal_resolve_product",
"arguments":{"identifier":"C4 Original"}}}'What the transport does and does not do
- Protocol versions: 2025-06-18, 2025-03-26, 2024-11-05. An unsupported
MCP-Protocol-Versionheader is a 400 with the supported list in the body. - Stateless. No
Mcp-Session-Idis issued, so there is no session to lose and nothing to resume. Every request carries its own key. - JSON, not SSE. Every answer is a single bounded result with nothing to stream, so a POSTed request gets
application/jsonback. AGETreturns 405, which is how the specification says a server declines to open a server-initiated stream. - No resources and no prompts. Both list empty on purpose. Everything here is a live call against the Record, and a resource would be a copy of Record state sitting in your context with no request behind it and no date on it.
- Server-side only. A request carrying a cross-origin
Originheader is refused, which is the DNS-rebinding protection the specification requires. A browser page cannot call this endpoint, and it should not: the key would be in the page.
What the agent gets
Tool descriptions are generated from the same capability manifest the REST API and the OpenAPI document read, so a tool cannot mean one thing here and another there. Each carries what it is for, what it cannot provide, and its limits, because those are the three things an agent gets wrong without them.
| Tool | Use it for |
|---|---|
formulasignal_search_record | Answer one specific, bounded question from FormulaSignal's covered pre-workout Record. |
formulasignal_resolve_product | Resolve a brand, product name, or alias to one canonical covered product. |
formulasignal_get_product_record | Return the controlled current Record for one covered product: identity, serving configuration, declared ingredients, captured price context, and freshness. |
formulasignal_get_formula_history | Return the controlled historical timeline for one covered product, with each state classified by what it can actually support. |
formulasignal_explain_signal | Explain one approved FormulaSignal Signal: a confirmed, reviewed change between two comparable product states. |
formulasignal_list_signal_changes | Return approved FormulaSignal Signals in release order, newest first, for incremental polling. |
formulasignal_compare_products | Compare two or three covered products on consistent FormulaSignal criteria. |
formulasignal_get_serving_economics | Return the captured commercial facts and the deterministic price arithmetic for one covered product. |
formulasignal_get_research_context | Return the dose range used in the selected evidence set for named ingredients, with the citation and its limitations. |
formulasignal_get_regulatory_context | Return regulatory records and documented cautions for named ingredients, each carrying the class of record it actually is. |
formulasignal_get_category_snapshot | Return a bounded summary of approved Signals and coverage state across the covered pre-workout set for a date window. |
formulasignal_list_ledger_editions | List the published Category Ledger editions: period, status, data-as-of date and the count released in each. |
formulasignal_get_ledger_edition | Return one Category Ledger edition: the executive summary, confirmed changes released in the period, category benchmarks with their cohorts, serving economics, the product comparison, and the limitations. |
formulasignal_list_watched_products | Return the watchlist of the one Founding Pro account this key is bound to, with each product's monitoring state. |
formulasignal_watch_product | Add one covered product to the bound account's watchlist. |
formulasignal_unwatch_product | Remove one product from the bound account's watchlist. |
formulasignal_get_watch_receipt | Return the monitoring receipt for one period: valid checks, attempts that returned nothing, recoveries, and confirmed Signals. |
formulasignal_get_record_version | Return the public Record version, the methodology version, the supported category, coverage counts, and source freshness. |
Four of these need a scope this page’s free key does not carry. list_ledger_editions and get_ledger_edition need ledger:read, and the three watch tools need watch:manage. They are listed because an agent should know they exist and refuse cleanly, rather than discover them by failing.
Instructing your agent
The tool descriptions already carry the semantics, so this is reinforcement rather than the load-bearing part. Never make correct use depend on a system prompt somebody can edit out.
SYSTEM PROMPT FRAGMENT
When answering about a supplement product:
- Resolve the name with formulasignal_resolve_product first. If it
returns candidates, ask which one. Do not pick.
- Quote the observation date with any value you state.
- An observation window is not a change date. If asked exactly when a
product changed, give the window and say the exact date is not
established.
- An ingredient absent from a declared list is undisclosed, not absent.
A null amount is not zero.
- Price per serving is a FormulaSignal calculation, not a manufacturer
figure. Say so.
- Report a refusal as a fact about the Record, not about the product.
"FormulaSignal has no confirmed change on file" is not "the product
never changed".The evidence model explains each of those rules and why the Record draws the line where it does.