ForegroundApp, register tools with a decorator, and call app.run().
Foreground apps serve MCP over streamable HTTP.
stdio transport is not supported for deployed apps.If you’re new to MCP, start with the protocol docs.Foreground patterns in the examples
There are two common ways to build a foreground app:
Prefer the native
ForegroundApp pattern when you are writing the integration yourself. Use the remote MCP wrapper pattern when porting an existing MCP server or when the provider hosts an MCP endpoint.
Minimal example
A complete, working foreground app that exposes one tool:The ForegroundApp class
Subclass ForegroundApp and call super().__init__(name, logger_name=...). The constructor sets up the MCP server machinery, your logger (self.logger), and the internal tool registry.
name— a short identifier for your app, used in MCP server metadata.logger_name— optional; defaults to the app name. Use a namespaced logger so your logs are distinguishable in the runtime.
Registering tools
Tools are async functions registered via the@self.tool(...) decorator. The decorator takes a ToolSpec with the tool’s name, description, and icon:
ToolSpec:
Icons
Passing a name like"search" is equivalent to calling phosphor_icon_url("search") — the SDK turns it into a Phosphor SVG URL. If you want a custom icon, pass the full URL instead:
Type hints are your tool schema
The tool function’s type hints become the MCP tool’s input schema. Keep them concrete:str, int, float, bool, list[...], dict[...], Optional[...]. Give every parameter a sensible default where possible — the model can call the tool with fewer arguments if you do.
Wrapping a remote MCP server
Some apps are mostly adapters around another MCP server. The wrapper still matters because it is where you make the provider useful and safe for the Truffle agent:- add or override tool titles, descriptions, icons, and annotations
- normalize provider-specific argument names into model-friendly arguments
- strip debug-only fields from tool results
- convert verbose provider JSON into compact markdown plus small
structuredContent - report auth/runtime errors through Truffle’s app error path
notion example is a remote MCP proxy with OAuth:
viator example goes further: it wraps a remote MCP server, overrides the two public tool schemas, adds local arguments like include_raw, and formats compact travel-search results before returning them.
Use this pattern when an upstream MCP is already good enough to call, but not good enough to expose raw.
Returning results
For simple action tools, returning a small dict withok() / err() is fine:
CallToolResult with:
- concise markdown in
content, optimized for the model to read - small
structuredContent, only for IDs or fields the model may need for follow-up calls - no duplicated status/tool-name fields
- no raw provider payloads unless the user explicitly asks for a detailed/debug view
structuredContent to stable IDs, refs, pagination, or fields required for follow-up calls.
Error handling
For errors you expect (bad inputs, HTTP failures, rate limits), returnerr(...). For authentication failures — where the user needs to reinstall or refresh credentials — raise AppAuthError:
AppAuthError— the runtime classifies this as an auth problem and flags the app as needing user intervention. Use it for 401/403 responses, expired tokens, or missing credentials.AppRuntimeFailure— for generic runtime issues you want to propagate instead of returning as a soft error.- Returning
err(...)— for expected errors the agent should see and potentially recover from (bad user input, rate limits, upstream 5xx, etc.).
Tool design guidance
A few things we’ve learned from building the reference apps:- Validate inputs early. If the caller passes
limit=99999, clamp it. If an enum arg is invalid, returnerr(...)with the valid values. - Optimize result text for the model. For read tools, concise markdown beats raw provider JSON. Keep structured payloads small and action-oriented.
- Write descriptions the model can route on. “Search X for Y. Use this when Z.” Describe when to use the tool, not just what it does.
- Keep one concept per tool.
get_marketandcreate_orderare better than a singlemarket_opwith amodearg. - Name parameters clearly.
tickernott,include_textnottext.
Testing with truffile infer
You can point the inference REPL at your local MCP server and exercise it end-to-end before deploying.
-
Start your app locally:
It’ll listen on
http://127.0.0.1:8000/mcpby default. -
In another terminal, open the infer REPL:
-
Connect to your MCP server and list the tools:
-
Run a real prompt that should trigger one of your tools:
Cleanup on shutdown
Close any HTTP clients, database connections, or subprocesses on exit:Deploy
tool_calls in the JSON result. The model may discover the installed
tool through normal routing, but Convo cannot attach or restrict an app per
chat, so this is not deterministic enforcement. Before deployment, use
truffile infer --mcp <url> when you need to select and test the local MCP
server directly.
When to build a foreground app
Build a foreground app when you need:- Request/response tools the agent calls during a Convo request (search, fetch, lookup, CRUD operations)
- Deterministic API integrations the model should reach for in specific situations
- Action-style behaviors (post, create, cancel, trigger)
