Documentation / Agent access

Connect the agent. Keep the engine singular.

Tactfolio exposes its canonical strategy builder and backtest engine through one hosted MCP endpoint. Codex, Claude, OpenCode, and compatible clients authenticate in your browser—there is nothing to install and no API key to copy.

OAuth browser login14 MCP tools1 hour access tokens0 local packages

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.

01 / CLIENTYour agentCodex, Claude, OpenCode
02 / ACCESStactfolio.com/mcpOAuth + Streamable HTTP
AUTHORCanonical builder commandsSame factories and mutations as the UI
RESEARCHTactfolio API and engineValidation, persistence, books, backtests
No secrets in configuration.

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.

Research comparisons fail closed.

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.

Hosted Streamable HTTP endpoint
https://tactfolio.com/mcp

Codex CLI, desktop, or IDE

Register the URL, then start OAuth login. The second command opens Tactfolio in your browser.

Shell · Codex
codex mcp add tactfolio --url https://tactfolio.com/mcp
codex mcp login tactfolio

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

Shell · Claude Code
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.

JSON · opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "tactfolio": {
      "type": "remote",
      "url": "https://tactfolio.com/mcp",
      "enabled": true
    }
  }
}
Shell · OpenCode
opencode mcp auth tactfolio
opencode mcp list

Other MCP clients

Use the client’s “remote MCP”, “Streamable HTTP”, or “custom connector” form. Standards-compliant clients discover OAuth automatically from the unauthenticated challenge.

Generic remote MCP settings
Server name: Tactfolio
Transport: Streamable HTTP
URL: https://tactfolio.com/mcp
Authentication: OAuth / automatic discovery
Headers: none

Clients 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

  1. Your client opens Tactfolio’s sign-in page.
  2. Sign in or create an account. Your password is submitted only to Tactfolio.
  3. The consent page identifies the client and explains the requested strategy/research access.
  4. Choose Allow access. The authorization code returns only to the exact redirect URI registered by the client.
  5. 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.
Compatibility is automatic.

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:

Prompt · connection check
Call connection_status.
Confirm that the API is ready and report the authenticated email.
Then list the available Tactfolio tools.
API readyCanonical service responds
AuthenticatedYour account is resolved
14 toolsBuilder and research available

04 / BUILD

Build and backtest a first strategy

Prompt · starter 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.
01ContractRead live rules
02CreateOne scaffold
03MutateBatch edits
04ValidateCompile graph
05BacktestInspect evidence
06SaveAfter approval

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.

Tool input · mutate_strategy
{
  "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.

Prompt · controlled variant sweep
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.
01HypothesisPredeclare objective
02SweepOne variant call
03SelectUse stated rule
04ConfirmUntouched window

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.

Tool input · save_strategy update
{
  "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.

Read the declared semantics.

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

01

Author

get_builder_contract
create_strategy
inspect_strategy
mutate_strategy
02

Research

validate_strategy
backtest_strategy
backtest_variants
get_latest_book
search_tickers
03

Library

list_strategies
get_strategy
save_strategyWRITE
delete_strategyDESTRUCTIVE
04

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.

Tool input · delete_strategy
{
  "strategyId": "<server-owned strategyId>",
  "baseRevisionId": "<current revisionId>"
}
Know the canonical boundary.

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.

Agent Auth discovery
https://tactfolio.com/.well-known/agent-configuration

Agent Auth grants appear beside MCP OAuth connections in Settings → Connected agents and can be revoked independently.

08 / SECURITY

Credential and execution boundaries

01

Short-lived access.OAuth access tokens expire after one hour and are cryptographically bound to https://tactfolio.com/mcp.

02

Rotating refresh.Refresh tokens rotate and expire after 30 days. Clients retry safely through a narrow replay window.

03

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.

04

Least privilege.The grant covers builder, private strategy, and research operations—not broker, admin, editorial, or account settings.

05

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.

06

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 ↗

Shell · recover OpenCode local database
# 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 tactfolio

The 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

Sharpe uses a zero-return baseline.

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.