McpAgentTools.h header
#include <ew/app/ai/agent/McpAgentTools.h>
Namespace ew::app::ai::agent
Functions
QString ew::app::ai::agent::mcpAgentSystemPrompt(bool allowWrites)
What the assistant is told about the project and its tools, for a session that may write when allowWrites is true and may only read otherwise.
It states the addressing idiom and the undo model, like the MCP server's own instructions, and in read-only mode says plainly that the write tools are absent – so a model asked to make a change explains that the switch is off instead of hunting for a tool that will never appear.
std::vector< AiToolSpec > ew::app::ai::agent::mcpAgentTools(const mcp::McpServer &server, bool allowWrites)
The tools server advertises, as provider-agnostic AiToolSpecs to attach to an AiRequest. With allowWrites false, only readOnlyMcpToolNames are included.
This is what gives the in-app assistant the same reach as the MCP server rather than a hand-picked subset of it. Bridging the catalogue instead of re-declaring it is the point: the assistant used to have three read tools and could not see a document at all, so asked to summarize the manuscript it searched the codex for something called "documents" and then invented a category. Any tool added to the server from now on reaches the assistant with it, and the two cannot drift.
Each MCP input schema becomes the AiToolSpec's parameters verbatim – both are JSON Schema, so nothing is translated and nothing can be lost in translating it.
QString ew::app::ai::agent::mcpToolSubject(const QString &toolName)
The subject a tool acts on, taken from its name: the first word after a leading verb.
get_timeline, update_timeline and create_timeline_event all answer "timeline". Derived rather than tabulated so a tool added later joins its family without anyone maintaining a list – a hand-kept table would be exactly the thing that goes stale and silently splits a subject in two.
const QSet< QString > & ew::app::ai::agent::readOnlyMcpToolNames()
The MCP tools the assistant may use when it has NOT been given permission to change the project: pure reads, and generators that compute an answer without touching anything.
Named explicitly rather than derived, because nothing about a registration reliably says whether its tool writes. The obvious test – does the handler reach runMcpMutation – is wrong: several tools, find_replace among them, delegate to a helper that calls it, so the handler body looks read-only while the tool rewrites prose across the whole manuscript. Guessing that wrong would let the assistant edit a project whose owner had switched that off, which is the one thing the switch exists to prevent.
A tool NOT in this set requires act mode. That is the safe direction, and it is deliberate: a tool added later that nobody classifies is unavailable in read-only chat rather than silently able to write. It also covers tools that change nothing in the project but still write to disk – the exports, backup_project, save_content_pack – and undo_last, which changes the project by taking a change away.
std::vector< AiToolSpec > ew::app::ai::agent::selectAgentTools(const std::vector< AiToolSpec > &specs, const QString &request, std::size_t budget=kToolBudgetPerRequest)
Picks the tools worth sending for request, keeping at most budget of them.
WHY SELECT AT ALL. Offering everything is what made 128 a ceiling; choosing per request removes it rather than buying headroom, so the catalogue can grow without any request growing.
The rules, in the order they matter:
A floor that is always sent: search, the project overview, the manuscript tree, the two get_* tools everything else refers to, and the undo pair. Without these a model cannot orient itself, and no request text reliably asks for them by name. Whole subjects, never halves. If anything about the timeline scores, every timeline tool goes read and write together. A partial subject is worse than an absent one: the model starts a task it cannot finish and leaves the project half-edited. Named tools are always included, so a request that says find_replace gets it whatever the scoring thinks. Remaining budget goes to subjects by score. Ties break by name, so one request always produces one request.
With an empty request there is nothing to score, so this returns the whole catalogue when it fits and the floor plus the largest subjects when it does not – never nothing.
Constants
std::size_t ew::app::ai::agent::kMaxToolsPerRequest = 128
The most tools one provider request may carry.
Not an MCP limit: MCP itself has none, and ordfoss-mcp could expose three hundred tools to Claude Desktop quite happily. It is the per-request function cap of OpenAI-compatible providers, and it bound this product only because act mode used to offer the ENTIRE catalogue in a single request – a fact about our own design rather than about the protocol.
std::size_t ew::app::ai::agent::kToolBudgetPerRequest = 64
How many tools a request is aimed to carry once selection has run.
Deliberately well under kMaxToolsPerRequest rather than at it. Sitting on the cap is how the catalogue came to be exactly 128 with the next tool failing the build, and a model choosing among sixty-four functions chooses better than one choosing among a hundred and twenty-eight.