00 / BOUNDARY
One access layer, one source of truth
The MCP server is deliberately thin. It registers typed tools, authenticates the account, and delegates each request. It does not define a second AST, clone the visual builder, or contain another backtester.
The client discovers Tactfolio’s authorization server, opens your browser, and stores its own revocable OAuth grant. Never add a bearer header, environment token, or account password.
comparison.comparable is false when fewer than two candidates complete, a realized start or end is missing, realized windows differ, price-coverage boundary constraints differ, or shared market-data inputs lack matching canonical revision evidence. Read comparison.reasonCodes and comparison.message; never rank failed rows.
01 / CONNECT
Connect your client
Every client uses the same hosted endpoint. Choose its native setup below.
https://tactfolio.com/mcpCodex CLI, desktop, or IDE
Register the URL, then start OAuth login. The second command opens Tactfolio in your browser.
codex mcp add tactfolio --url https://tactfolio.com/mcp
codex mcp login tactfolioCodex desktop and the IDE extension share the same MCP configuration on that host. You can also add the URL from Settings → MCP servers, then use codex mcp login tactfolio.
Claude Code
Add a remote HTTP server, then use claude mcp login tactfolio. You can also open /mcp inside Claude Code and choose Authenticate.
claude mcp add --transport http tactfolio https://tactfolio.com/mcp
claude mcp login tactfolio
# Or start Claude Code, run /mcp, and choose Authenticate for tactfolio.OpenCode
Add this entry to opencode.json, then trigger browser authorization. OpenCode’s documented per-server timeout controls initial tool discovery, so it is intentionally omitted rather than presented as a backtest deadline.
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"tactfolio": {
"type": "remote",
"url": "https://tactfolio.com/mcp",
"enabled": true
}
}
}opencode mcp auth tactfolio
opencode mcp listOther MCP clients
Use the client’s “remote MCP”, “Streamable HTTP”, or “custom connector” form. Standards-compliant clients discover OAuth automatically from the unauthenticated challenge.
Server name: Tactfolio
Transport: Streamable HTTP
URL: https://tactfolio.com/mcp
Authentication: OAuth / automatic discovery
Headers: noneClients may namespace displayed tool names with the configured server name. For example, OpenCode can show tactfolio_backtest_variants; it is the same advertised tool.
02 / AUTHORIZE
Approve access in your browser
- Your client opens Tactfolio’s sign-in page.
- Sign in or create an account. Your password is submitted only to Tactfolio.
- The consent page identifies the client and explains the requested strategy/research access.
- Choose Allow access. The authorization code returns only to the exact redirect URI registered by the client.
- The client receives a one-hour, audience-bound access token and a rotating refresh token. Refresh is automatic while that grant remains active; after expiry or revocation, the client asks you to sign in again.
Tactfolio supports PKCE, protected-resource discovery, authorization-server discovery, Client ID Metadata Documents, and Dynamic Client Registration fallback. This covers modern and older OAuth-capable MCP clients without vendor-specific credentials.
03 / VERIFY
Verify the connection
Start a new agent session so its tool catalog refreshes, then send this prompt:
Call connection_status.
Confirm that the API is ready and report the authenticated email.
Then list the available Tactfolio tools.04 / BUILD
Build and backtest a first strategy
Use Tactfolio to build and research this strategy:
- Name: Risk-on with treasury defense
- If SPY is above its 200-session moving average, equal-weight SPY and QQQ.
- Otherwise allocate 100% to BIL.
- Rebalance monthly with 5 bps slippage.
- Read the compact builder contract, then request only the condition and node sections you need.
- Verify SPY, QQQ, and BIL in one batched ticker call.
- Validate, then backtest from 2016-01-01 through 2025-12-31.
- Use executionTiming next_open so each close signal fills at the next exact session open.
- Report CAGR, maximum drawdown, Sharpe, turnover, final allocation, and warnings.
- Do not save until I approve.Create the scaffold once and carry the latest documentToken, when present, into later draft-oriented calls while authoring an unsaved strategy; otherwise use the returned document. The token is a stateless compressed form of the same canonical document—not a credential or a second document format. inspect_strategy returns a bounded canonical outline by default: page shallow nodes with offset/limit, focus a known nodeId, or request responseMode: "full" only when the complete tree is necessary. When inspection says documentTokenStatus: "reuse_input", retain the submitted token instead of expecting it to be echoed. mutations is an atomic ordered batch of 1–256 commands. Bind a created node or condition with as: "gate", then use @gate later in that same batch; compound nodes expose handles such as @gate.then, @gate.condition, @state.enter, and @state.exit. Specified patch.weights may use aliases as keys in that batch, while slotWeights stays positional. A new Rank must be populated before a later update_node sets a keeper count above one or non-equal weighting. Compact responses carry the next token, bindings, changes, and structural summary without repeating the full document; validate, get, and save expose it consistently as result.documentToken. Never reconstruct the AST or guess an id. The scaffold id is temporary. The first save returns the server-owned strategyId and revisionId; on an update, pass that strategyId and the latest returned revisionId as baseRevisionId. Pass both ids to get_strategy to inspect that exact immutable artifact. save_strategy creates state; delete_strategy permanently removes it and should run only on an explicit user request.
Address a condition through its owner
update_condition and remove_condition always require both identifiers: nodeId is the owning If or EnterExit node, and conditionId is the condition inside it. The aliases below work after inserting an If with as: "gate" earlier in the same ordered batch. For a later call, use the concrete ids returned in bindings or changes.
{
"documentToken": "<latest documentToken>",
"mutations": [
{
"type": "update_condition",
"nodeId": "@gate",
"conditionId": "@gate.condition",
"patch": {
"lhs": { "indicator": "price", "lookbackDays": 1, "symbol": "SPY" },
"comparator": "is above",
"rhs": { "indicator": "movingAverage", "lookbackDays": 200, "symbol": "SPY" },
"persistenceDays": 1
}
}
]
}05 / RESEARCH
Move from hypothesis to confirmation
A useful agent should make the research decision inspectable before it sees sweep results. After inspecting the strategy, name the hypothesis, the one field each variant changes, the primary metric, the tie-breaker, the execution timing, and the date windows.
Research which trend-filter lookback gives this strategy the best risk-adjusted result.
- Inspect the current strategy, then state the hypothesis before seeing sweep results.
- Development window: 2016-01-01 through 2021-12-31.
- In one backtest_variants call, compare the unchanged 200-session base document
with 150 and 250-session filters. Change only the lookback.
- In that call, set selection.primary to maximize Sharpe, add maxDd >= -0.35 as
a hard eligibility gate, and use maximize maxDd (closest to zero) as the tie-breaker.
- Set confirmationWindow to 2022-01-01 through 2025-12-31 so only the
development winner touches the holdout.
- Use executionTiming next_open throughout and keep continuationTokens at winner.
- Explain invalid or failed candidates; do not silently replace them.
- Report the comparable development leaderboard, selection decision,
winner-only confirmation, researchEvaluationDraft, and limitations. Do not save.backtest_variants applies up to 12 labeled mutation batches through the canonical builder, then runs the authoritative backtester. Give it a machine-readable selection plan with a primary metric and direction, hard eligibility gates, and ordered tie-breakers. When explicit development dates and a later confirmationWindow are present, it ranks only candidates whose realized windows, boundary constraints, execution timing, and canonical revision evidence for shared market-data inputs are comparable, then runs a confirmation backtest only for the development winner. Its automatic Base document (unchanged) candidate is exactly the input document—not an earlier starter. Candidate results are compact scorecards; curves, allocation histories, and books remain available through backtest_strategy on the winner. Shared evidence is emitted once, retryable resource pressure receives bounded automatic retries, and progress is reported to clients that request it. A 165-second internal deadline returns completed evidence and identifies unfinished labels before a client configured for 180 seconds gives up. Selection fails closed if any requested candidate is invalid or failed; allowPartial is an explicit unsafe opt-in because it introduces completed-subset survivorship bias. Keep partial scorecards as evidence, then rerun the full sweep and require comparison.comparable=true before selecting. continuationTokens defaults to winner, so the selected continuation appears once at selection.winner.documentToken; choose all only when every changed draft is immediately needed. If the base wins, retain the base token or document. If no candidate clears the declared gates, confirmation is skipped. researchEvaluationDraft records the actual windows, execution timing, selection rule, and selected label for the later save handoff. Tactfolio never hides rejected or failed candidates and does not persist the experiment.
Both backtest tools accept date windows as start / end or startDate / endDate. If both forms are supplied, their values must match. Tool arguments are strict, so an unknown name fails loudly instead of being ignored and silently changing the experiment. MCP backtests default executionTiming to next_open; provide same_close only for a deliberate legacy comparison.
Save the evidence with the exact revision
After approval, save_strategy can attach self-assessed research metadata owned by the new immutable revision. The strict version 2 contract records the hypothesis, primary source URLs, fidelity label, caveats, development and confirmation windows, the execution timing of each window, selection rule, and selected variant. Version 1 remains readable as legacy metadata but cannot identify its execution model. This is owner-supplied context, not a Tactfolio attestation that the source was reproduced or the plan was predeclared. It lives outside the strategy AST and has no effect on execution or backtest results.
{
"documentToken": "<winning documentToken>",
"strategyId": "<strategyId>",
"baseRevisionId": "<latest revisionId>",
"research": {
"version": 2,
"hypothesis": "A long-term trend filter can reduce severe drawdowns.",
"sources": [
{
"url": "https://papers.ssrn.com/sol3/papers.cfm?abstract_id=962461",
"title": "A Quantitative Approach to Tactical Asset Allocation"
}
],
"fidelity": "partial",
"caveats": [
"The two-asset risk-on sleeve is a material variant of the cited rule."
],
"evaluation": {
"development": {
"start": "2016-01-01",
"end": "2021-12-31",
"executionTiming": "next_open"
},
"confirmation": {
"start": "2022-01-01",
"end": "2025-12-31",
"executionTiming": "next_open"
},
"primaryMetric": "Sharpe",
"tieBreakers": ["Maximum drawdown closest to zero"],
"selectedVariant": "150-session filter"
}
}
}The research envelope is replaced as one object, never merged field by field. On an update, provide the complete research object to replace it, omit research to inherit the base revision’s metadata, or send research: null to clear it in the new revision. On a first save, omission means no research metadata. get_strategy returns the metadata for the selected exact revision.
With the MCP default next_open model, each official-close target waits for the next exact provider-adjusted session open. Old holdings receive the overnight close-to-open return, trades and configured slippage occur at the open, and new holdings receive the open-to-close return. Missing exact open data fails closed; the engine never substitutes a carried price or a later close. Daily open is still a research proxy and does not model auction liquidity, spread, market impact, or order limits. The explicit legacy same_close model fills a close-derived target at that same close, so a rule that uses the current final close is non-causal/look-ahead in that mode. Returns and volatility are decimal fractions. Maximum drawdown is negative, so closer to zero is smaller. Turnover is an annualized one-way fraction, so 5.1 means about 510% of value traded per year. trace diagnostics such as false_condition show normal routing; warnings are actionable. backtest_strategy summarizes diagnostics by default; request diagnosticDetail: "full" there for the complete audit trace. Variant comparison always stays compact. lastBook is the last allocation actually filled; pendingTargetBook is a final target still waiting for its next session open. lastBookContext separates signal generation, fill, and the final session through which actual holdings were valued. Provenance explains when exact historical replay is unavailable.
06 / TOOLS
Tool reference
Author
get_builder_contractcreate_strategyinspect_strategymutate_strategy
Research
validate_strategybacktest_strategybacktest_variantsget_latest_booksearch_tickers
Library
list_strategiesget_strategysave_strategyWRITEdelete_strategyDESTRUCTIVE
Diagnose
connection_status
The compact contract index advertises canonical local and resolved-graph limits. The global tool catalog keeps mutation items generic for a smaller cross-client discovery payload, while runtime validation remains exact. Before guessing a builder command, request get_builder_contract with section: "mutations" and one mutationType; it returns the exact accepted schema and a copy-ready example. A malformed command returns its one-based mutation index, known type, and direct repair action. In particular, set_document patches only the name and settings—it never replaces the AST. Request the conditions section for formal ticker, ratio, rolling-correlation, and external-series operand schemas and unit rules. Verify up to 200 exact symbols in one search_tickers call; matched rows include actual priceCoverage dates when history is present. list_strategies supports compact pagination plus name, cadence, and virtual-NAV safety filters. If ids are already known, batch up to 100 in strategyIds so a full AST or research read does not overfetch the catalog. Complete rows stay in structured content while the text fallback remains a compact index.
Delete a saved strategy
First read the owned strategy with list_strategies or get_strategy, then copy its current revisionId into baseRevisionId. This concurrency guard makes a stale agent fail instead of deleting a strategy changed elsewhere. The canonical API also refuses deletion while a trading deployment uses the strategy. A successful call removes the strategy and all of its revisions permanently.
{
"strategyId": "<server-owned strategyId>",
"baseRevisionId": "<current revisionId>"
}The portfolio layer is long-only and cannot borrow, short, or synthesize leverage. Long holdings of supported embedded-leverage ETFs such as UPRO, TQQQ, and TMF are allowed and use the fund’s actual post-inception adjusted history—not synthetic pre-inception data or a guaranteed multi-day multiple. Each Rank scores one indicator; pair correlation is a condition compared with a numeric scalar, not another correlation or a composite rank factor. Weighting supports equal, specified, and inverse volatility—not covariance optimization or portfolio-volatility targeting. One document also has one global cadence. Inputs are supported US-listed ticker histories rather than continuous futures or forwards. The currently tradable catalog is not a survivorship-free historical universe. An agent should label the closest engine-native implementation proxy or partial with concrete caveats, never hide a missing paper mechanic in the MCP adapter.
The MCP can build, validate, research, read, save, and delete owned private strategies. It cannot publish, connect brokers, place trades, administer accounts, or operate portfolios.
07 / AGENT AUTH
Native Agent Auth protocol
Tactfolio also publishes /.well-known/agent-configuration using Better Auth’s Agent Auth plugin. Clients that implement Agent Auth can discover named capabilities, register an Ed25519 identity, request scoped grants, and use device authorization. They call the same canonical builder commands and Tactfolio API as MCP.
https://tactfolio.com/.well-known/agent-configurationAgent Auth grants appear beside MCP OAuth connections in Settings → Connected agents and can be revoked independently.
08 / SECURITY
Credential and execution boundaries
Short-lived access.OAuth access tokens expire after one hour and are cryptographically bound to https://tactfolio.com/mcp.
Rotating refresh.Refresh tokens rotate and expire after 30 days. Clients retry safely through a narrow replay window.
No token passthrough.The MCP token is verified only at the MCP boundary. Tactfolio creates a one-minute server-signed internal identity for calls to its own API.
Least privilege.The grant covers builder, private strategy, and research operations—not broker, admin, editorial, or account settings.
Explicit writes.save_strategy creates revisions; delete_strategy is marked destructive and requires the exact current strategy revision. Ask the agent to wait for your approval before either operation.
Revocable.Open Settings → Connected agents to revoke an MCP consent or Agent Auth identity.
09 / FIX
Troubleshooting
The client reports HTTP 401
This is the expected discovery challenge before login. Run the client’s login/auth command or choose Authenticate in its MCP screen. Do not add a manual Authorization header.
OpenCode says “Database is not empty and has no session table”
This is an OpenCode-local database guard that runs before OAuth; Tactfolio has not received an authorization request. First check whether OPENCODE_DB accidentally points at another application’s SQLite database. If there is no override, close OpenCode and move the invalid database plus its WAL files into a recovery directory before retrying. The commands below preserve the original files instead of deleting session data. OpenCode’s migration source ↗
# Close OpenCode first. An override must never point at another app's database.
printenv OPENCODE_DB
opencode db path
# If OPENCODE_DB is an unintended override:
unset OPENCODE_DB
opencode mcp auth tactfolio
# If no override is set and the same error remains, preserve the invalid local DB:
db_path="$(opencode db path)"
recovery_dir="${db_path}.recovery.$(date +%Y%m%d%H%M%S)"
mkdir "$recovery_dir"
mv "$db_path" "$recovery_dir/"
[ ! -e "${db_path}-wal" ] || mv "${db_path}-wal" "$recovery_dir/"
[ ! -e "${db_path}-shm" ] || mv "${db_path}-shm" "$recovery_dir/"
opencode mcp auth tactfolioThe browser does not open
Copy the authorization URL printed by the client into a browser on the same machine. Loopback redirect URIs return the result to the client.
Consent returns to an error page
Retry login from the client. Authorization requests and codes are short-lived and PKCE verifiers belong to the client that initiated them.
The client still uses an old connection
Remove its saved MCP authorization, add the server again, and restart the client. Revoked access cannot be repaired by reusing cached tokens.
Research times out
If your client exposes a tool-execution deadline, set it to at least 180 seconds for long backtests. OpenCode’s MCP timeout setting applies to tool discovery, not long tool execution. Narrow the date range while iterating, then run the final requested window.
10 / METRICS
Interpret the engine metrics literally
Tactfolio annualizes mean portfolio return divided by volatility and does not subtract a risk-free series. Explicit holdings such as BIL still earn their market return, so a BIL-heavy strategy’s Sharpe is not an excess-return Sharpe. Benchmark risk is not volatility-matched automatically; a smaller drawdown than unscaled SPY is not by itself a like-risk comparison. Read the machine-readable metricUnits object returned with every backtest before judging a hypothesis.