--- name: expose-tools-on-mcp description: Put a newly published tool in front of people by adding it to one exact MCP server in an explicit AICP project, creating a new server from the project's function tools when none fits, or take a tool back off, with confirmed and version-protected changes through the Speakeasy AI Control Plane Platform MCP. --- # Put a tool on an MCP server Use this workflow only through the authenticated Speakeasy AI Control Plane (AICP) Platform MCP connected as an external administrator. Publishing a tool to a project regenerates what the project can offer; it does not put that tool in front of anybody. Which tools one MCP server exposes is a separate, deliberate decision, and this workflow is how an administrator makes it: read the project's tools, read what the server exposes today, then add or remove named tools against the exact list that was read. When the project has no server that fits, the same workflow can create one whose tools are tools the project's functions already produce. This is not the same as adding a server to a plugin. A plugin decides which servers people receive; this decides which tools one of those servers offers. For putting a whole server in front of people, use `add-mcp-from-catalog`, `add-mcp-from-remote-url`, or the plugin workflows instead. It is not available to managed project assistants: the change reaches everyone holding an affected plugin the moment it commits, so it stays on the surface an administrator drives directly. Installing this package grants no organization or project access. ## Safety rules - Only a server whose tools come from this project can be changed here. A server that fronts an upstream offers whatever that upstream offers, and a server set up before AICP tracked servers separately has no record to change; both refuse with a message pointing to the AICP dashboard. Report that refusal and stop rather than looking for another route. - Keep the project, the MCP server, and every tool explicit. Never infer any of them from prior context, a display name, or the Default project, and never substitute a similar-looking tool. - Never guess a tool identifier. Use an exact identifier a tool returned: for an addition, the one from the project's tool list; for a removal, that or the one from the server's own `tool_exposure.tool_urns`. Never type one out from a display name or from memory. - The project's tool list governs additions only. A tool missing from it cannot be added, most often because the deployment that produces it has not finished; say so rather than adding something else. A removal is not bound by it: a tool the project has stopped producing can still be sitting on the server, and taking that orphaned entry off is exactly what a removal is for. - State the blast radius before acting. Changing a server's tool list republishes every plugin that carries that server, and everyone holding one of those plugins receives the change immediately. Removing a tool takes it away from those same people, including anyone in the middle of using it. - Every change carries the exposure version from the read it was based on, a fresh idempotency key, and `confirmed: true` only after the user has confirmed the exact server and the exact tools. Never send `confirmed: true` on the user's behalf. - Never retry a mutation on your own initiative. Every conflict, denial and refusal goes back to the user: report what happened and what it means, and act again only when they ask. Never retry against a different server, and never widen the tool list to make a call succeed. What changes between outcomes is what you take back to them — a fresh read for a conflict, a wait for a rate limit, a dead end for a structural refusal — never whether they are involved. - A refusal saying the tool list is shared beyond what this change can reach is final, not a race. It means the same set of tools is offered by more MCP servers than can be changed from here, or by a server outside the chosen project, so re-reading and repeating reaches the same answer every time. Report it, send the user to the AICP dashboard, and stop; never loop back to a fresh read on it. - Nothing is dropped silently. A tool this project does not produce and the server does not already expose refuses the whole request by name and changes nothing. A tool already in the state the user asked for is reported as unchanged, not as a change. - Secrets never enter chat. This workflow needs none; never ask for or accept credentials, tokens, headers, or authorization values, and never present a link a tool did not return. - Use `send_platform_mcp_feedback` only after asking for consent, and never include identifiers, URLs, credentials, payloads, or logs. ## Workflow 1. Call `list_projects` with `limit: 100` to confirm the Platform MCP is authenticated and obtain the eligible projects. If authenticated discovery is unavailable, stop and ask the user to complete or repair AICP OAuth. If `truncated: true`, report that project discovery is incomplete and hand off to the AICP dashboard. Ask the user to choose one exact project. 2. Call `list_project_tools` for that exact project. Use `query` to narrow by tool name or by the function or API document that produced it, and follow every `next_cursor` with `cursor` and the same filters until the list is complete. Each entry names the tool, its exact identifier, and the source it came from, so the user can tell two similarly named tools apart. If a tool the user wants **added** is absent, report that the project's latest completed deployment does not produce it yet and stop; publishing again and waiting for that deployment to finish is the fix, not a different tool. For a **removal**, do not stop here: a tool the project no longer produces can still be on the server, so carry on through step 3 — the user still has to choose the exact server — and then take the exact identifier from that server's own `tool_exposure.tool_urns` in step 4. 3. Call `find_mcp` for that exact project with `limit: 100`, following every `next_cursor` with `cursor` and no `query` until the list is complete. Ask the user to choose one exact MCP server. Do not choose for them and do not assume there is only one. If the project has no server, or the user says none of them fits, offer to create one instead and follow the next paragraph; never create a server the user did not ask for. **Creating a server.** This is for an addition only, and only from tools a function produced: every tool must come from `list_project_tools` with `source_kind` `function`. A tool generated from an API document cannot be used here; say so and point the user at the AICP dashboard. Ask the user for the new server's name. First call `create_mcp_from_functions` with the exact project ID, the name, the exact tool identifiers, one new idempotency key for this creation, and without `confirmed: true`; it creates and records nothing and answers `confirmation_required` with a `preview`. Never work out or describe a slug yourself. Show the user the `preview` values as returned before asking for confirmation: `toolset_slug` is exact, and `mcp_slug_prefix` is only the start of the server's address, because a short suffix is added at creation — say that, and never present the prefix as the full address. When `preview.would_join_default_plugin` is true, this would be the organization's first server: say plainly that it joins the project's Default plugin on creation, so everyone holding that plugin receives it. Present the exact project, the name, the preview, who will receive the server, and each tool by name and identifier, and ask for explicit confirmation. After they confirm, call `create_mcp_from_functions` again with the same project, name and tools, the same idempotency key the preview used, and `confirmed: true`. Use a new key only when starting a new attempt after a refusal. A refusal naming tools means nothing was created: those tools are not produced by the project's latest completed deployment, so return to step 2 rather than creating a server without them. A refusal saying the name is taken means nothing was created: ask the user for another name. On success, report the full `mcp_slug` the result returns and the returned `exposure` (a fresh read, not the list that was asked for), and read `index_signal` exactly as step 10 describes. Then say who receives the server from the result, never from the preview: when `added_to_default_plugin` is true, tell the user it joined the project's Default plugin and everyone holding that plugin receives it. `publication_requested` means a refresh of their plugin was requested, not confirmed delivered: never tell the user people already have the server. When `added_to_default_plugin` is false, say plainly that the new server reaches nobody until it is put into a plugin, and offer the plugin workflow as the next step. Skip steps 4 to 8 for a server you just created; it already exposes exactly the confirmed tools. 4. Call `get_mcp` with that project ID and that exact `mcp_id`. Read its `tool_exposure`. If `tool_exposure` is absent, this server's tools are not this project's to change: say so, point the user at the AICP dashboard, and stop. Otherwise present the tools it exposes today next to the tools the user wants added or removed, and say explicitly which of them are already in the requested state. Keep `tool_exposure.exposure_version` internally; it is the read this change will be checked against. If `tool_exposure.truncated` is true, the server exposes more tools than one read reports: say that the list shown is partial before asking for a decision. 5. Present the exact change and its reach before asking for confirmation: the project, the server, each tool by name and identifier, and every plugin listed in the server's `distributions`. State plainly that everyone holding one of those plugins receives the change immediately, and for a removal that they lose those tools. When `tool_exposure.shared_with_other_servers` is above zero, say how many other MCP servers offer this same set of tools and that the change moves all of them together; the change is allowed only with permission to change every one, and is refused outright otherwise. The count covers only servers in the chosen project, so a change can still be refused as shared beyond this workflow's reach even when it reads zero. Ask for explicit confirmation of that exact list. 6. After explicit confirmation, call `add_tools_to_mcp` — or `remove_tools_from_mcp` for a removal — with the exact project ID, the exact `mcp_id`, the exact tool identifiers, the `exposure_version` from the step-4 read, a fresh idempotency key, and `confirmed: true`. Send the identifiers exactly as they were returned — character for character from `list_project_tools` for an addition, or from the server's `tool_exposure.tool_urns` for a removal. 7. Read the result before reporting anything. `outcome: applied` with an empty `unchanged` means every named tool moved. `outcome: applied` with a non-empty `unchanged` is a partial result: name which tools moved and which were already in the requested state. `outcome: no_op` means nothing needed changing, so do not announce a change or a republish. `exposure` is a fresh read taken after the change committed; report that list, not the one that was asked for. When `snapshot_scope` is not `fresh_read_after_commit`, the change committed but could not be read back: say that rather than claiming the final state. 8. A refusal naming tools means nothing was changed at all, not that part of the request landed. Present the named tools, explain that they are neither produced by this project nor already on the server, and return to step 2. A conflict means the server's tool list changed between the read and the write, so the change was refused to avoid overwriting somebody else's edit: return to step 4, present the list as it stands now, and ask the user to confirm again against it. Never reuse the old exposure version, and never retry with the same idempotency key and different tools. A refusal saying the tool list is shared beyond what this change can reach is the one refusal that does not return to a fresh read: it is about how the servers are set up, not about a race, so report it, point the user at the AICP dashboard, and stop. A rate-limit refusal is neither a race nor a dead end: nothing was changed and the same call will work later, so say that it was throttled and that the change has not been made, and repeat it unchanged only when the user asks you to — never on a timer of your own. 9. Report the publication outcome separately from the change. `publication_request` and `publish_signal` describe whether a package refresh was requested, not that plugins or the people holding them have converged. Verify with a fresh `get_mcp` if the user needs the settled state, and never claim people already have the tool unless the returned live state supports that. 10. Check `index_signal` on every applied change, and say something when it is neither `requested` nor `not_required`. A server set to dynamic tool selection cannot list any tools at all while its current tool list has no search index, so `unavailable` or `request_failed` means the change landed but that server may answer nothing for a few minutes until the platform rebuilds the index on its own. Tell the user plainly, say it is expected to recover without action, and point them at the AICP dashboard if it does not. Never present this as the change having failed — it did not — and never retry the mutation because of it. 11. Removal is the counterpart of the same design and follows exactly the same steps, with the same boundary as step 8, except that its identifiers may come from the server's exposed list rather than the project's. A name that is neither produced by this project's latest deployment nor already on this MCP server is refused by name, so a mistyped identifier is never mistaken for "already gone". A tool the project does produce but the server does not expose is not refused: it comes back as `unchanged`, because there was nothing to remove. Report that as "it was not on the server", never as a refusal. A tool the project no longer produces can still be removed, because the server is still offering it — so a removal never waits on a deployment, and never asks the user to publish a tool again in order to take it away.