Two files change. That is the whole swap.
1. Declare the server
integrations.yml gains an mcp_servers: block. Everything above it — the
model group, the channels — stays exactly as it was:
mcp_servers:
- name: hubspot_crm
url: http://127.0.0.1:8931/mcp
tool_timeout: 15
name is the id the skills will reference. It must match byte for byte; a typo
here is caught at model load rather than at train time, for reasons chapter 3
gets into.
url has to be http or https. That is not a style preference — MCPServerSpec
rejects any other scheme outright, which rules out stdio servers entirely.
Chapter 3 covers what that means in practice.
Pointing at HubSpot’s own MCP server later is the same two lines:
mcp_servers:
- name: hubspot_crm
url: https://mcp.hubspot.com/anthropic
token: ${HUBSPOT_ACCESS_TOKEN}
2. Point the imports at it
Each skill names the tools it wants with mcp/<server>:<tool>. Here is the
complete diff for all three skills:
--- skills/identify_customer/skill.md
+++ mcp_variant/skills/identify_customer/skill.md
import_tools:
- - find_contact_by_email
+ - mcp/hubspot_crm:find_contact_by_email
--- skills/check_tickets/skill.md
+++ mcp_variant/skills/check_tickets/skill.md
+import_tools:
+ - mcp/hubspot_crm:list_open_tickets
--- skills/log_interaction/skill.md
+++ mcp_variant/skills/log_interaction/skill.md
+import_tools:
+ - mcp/hubspot_crm:add_timeline_note
That is every changed line in every skill. Six lines, all of them inside YAML frontmatter.
Note the asymmetry. identify_customer already had an import_tools: block,
because find_contact_by_email was a global tool it imported by name — so its
change is a substitution. The other two called local tools defined in their
own tools.py, which needed no import at all, so their change is an
addition. Different starting points, same destination.
What the model sees
get_bare_tool_name strips the prefix, so the model is offered
find_contact_by_email, not mcp/hubspot_crm:find_contact_by_email:
✓ identify_customer imports find_contact_by_email server=hubspot_crm llm sees
'find_contact_by_email'
This is the mechanical reason the instructions do not have to change. The
skill’s prose says “call find_contact_by_email”, and after the swap there is
still a tool called find_contact_by_email in scope, with the same arguments
and the same result keys. The routing changed underneath; the name did not.
What did not change
Worth listing, because the list is longer than the diff:
- Every line of instruction prose, in all three skills.
- The ordered block in
log_interaction—draft_summarythenwrite_note. - The
requires_confirmationconstraint, and both its utterances. memory.yml, at project level and in every skill.lib/hubspot.py, the CRM client. The MCP server calls straight into it.agent.yml, the persona, the rules.
lib/hubspot.py is the quiet one. The MCP server imports the same client the
REST tools used and calls the same three functions. The error taxonomy — the
difference between contact_not_found and crm_unreachable — is preserved
because it was never in the transport layer to begin with. It was in the client,
and the client did not move.
Run it
make mock # terminal 1: the mock CRM
make mcp-server # terminal 2: the MCP server in front of it
make mcp-swap # move the project onto MCP
make train && make chat
make mcp-status says which transport is live:
MCP — integrations.yml declares mcp_servers:
- mcp/hubspot_crm:list_open_tickets
- mcp/hubspot_crm:find_contact_by_email
- mcp/hubspot_crm:add_timeline_note
And make mcp-restore puts REST back. The swap is deliberately reversible: the
REST project is the baseline the swap is measured against, so it stays on disk
rather than being overwritten. Delete it and the lesson goes with it.
Verify with the engine, not just the eye
Before training, ask the engine whether it accepts the project:
uv run python -c "from pathlib import Path; \
from rasa.mantle.validation import validate_project; validate_project(Path('.'))"
It is free, offline, and needs no credentials. On the swapped project it passes, which means the imports parsed, the server block validated, and no skill is referencing a tool that no longer exists locally.
