| name | author-mcp-tool |
| description | Sub-skill of `compose-domain`. Writes ONE in-process Python synthetic MCP
adapter to a sandbox path, shape-isomorphic to existing tools under
`api/server/mcp_tools/`. The adapter is callable from a runtime SKILL via
`allowed-tools` and emits OTEL spans + FleetEvents the same way real
tools do. Returns deterministic synthetic data — no real backend.
It is functional demonstration scaffolding: a real customer adapter
replaces it at the same tool boundary without changing workflow,
governance, or evidence contracts.
|
| audience | design-time-only |
| forbidden-runtime | true |
author-mcp-tool
You write one Python file per invocation: an in-process synthetic MCP
adapter.
The adapter mirrors the shape of api/server/mcp_tools/claim_lookup.py and
api/server/mcp_tools/policy_search.py exactly. It is callable from a
runtime SKILL via the GHCP SDK's tools=[…] channel and produces OTEL
spans the same way real tools do — so the Observatory animates correctly
without anyone having to plug in a real backend.
Semantic boundary. This adapter is functional demonstration scaffolding,
not an unfinished implementation. It returns deterministic synthetic data so
reference runs require no customer system. When a customer connects their
estate, they replace this adapter at the same tool boundary — the workflow,
governance evidence contracts, and HITL policies do not change.
Inputs you require from the caller
output_path — absolute path inside the sandbox, e.g.
tools/scratch/compose-domain/<run-id>/api/server/mcp_tools/<tool>.py.
tool_brief — the relevant external_systems[] entry from the
YAML brief:
id: <snake_case>
mcp_tool: <snake_case Python module name>
operations: [<snake_case function name>, ...]
canonical_example_path — one absolute path. v1 canonical is
api/server/mcp_tools/claim_lookup.py (HTTP-backed pattern with the
in-memory escape hatch). The three v1 graduated adapters
(workday_hr_employee.py, concur_travel_policy.py,
concur_travel_search.py) are working examples of the in-memory
shape this skill produces.
If any input is missing, stop and ask.
Note on standalone smoke testing
Importing any module under api/server/mcp_tools/ triggers
api/server/mcp_tools/__init__.py, which transitively imports
api/server/state.py → BlobStore, which connects to Azurite at
localhost:10000 on construction. Standalone import-smoke of a generated
tool therefore requires Azurite running:
azurite --silent --location azurite-data \
--blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 &
Without Azurite, python -c 'import api.server.mcp_tools.<tool>' hangs
in retry loops until SIGINT. This is a property of the existing harness,
not of the generated adapter.
Procedure
Step 1 — Read both canonical examples
Read canonical_example_path end-to-end. Note:
- the module docstring style (purpose + how it's exposed + a one-line
note about synthetic data vs production-grade).
- the
from copilot.tools import ToolResult, define_tool import.
- the
from ._otel import traced_tool import.
- the Pydantic params class convention (
_FooParams(BaseModel)).
- the
@define_tool(name=..., description=...) registration pattern.
- the
@traced_tool("<dotted.name>") wrapper on the underlying function.
You will mirror this shape. The three v1 graduated adapters
(workday_hr_employee.py, concur_travel_policy.py,
concur_travel_search.py) are good targets to compare against once
you've drafted yours — they are deterministic, self-contained, and
passed CHECKLIST §4.6–4.7.
Step 2 — Decide: HTTP-backed or in-memory?
For v1 we always pick in-memory deterministic synthetic MCP adapter. Reasons:
- The point of the generated domain is to animate in the Observatory and
exercise the orchestrator harness — not to integrate with a real
upstream system.
- HTTP-backed adds a Node mock server, port assignment, env var, and a
health check. All real engineering, all out of v1 scope.
If the operator/caller insists on HTTP-backed, stop. Tell them an HTTP-
backed tool is a separate workflow that lives outside this skill: it
requires authoring a Node mock server under mocks/<name>-mcp/, picking a
port, adding it to docker-compose.yml, etc. They should write that by
hand, then point a future revision of the in-memory adapter at it.
Step 3 — Build the body
Use the template at docs/superpowers/skills/compose-domain/templates/mcp_tool.py.tmpl
as the skeleton.
The shape:
"""<tool>.<operation> MCP tool — <one-line purpose>.
Synthetic MCP adapter generated by compose-domain. Returns deterministic
synthetic data keyed on the input(s). No real upstream call. Replace this
adapter at the same tool boundary when connecting the customer estate.
"""
from __future__ import annotations
import hashlib
import json
from copilot.tools import ToolResult, define_tool
from opentelemetry import trace
from pydantic import BaseModel, Field
from ._otel import traced_tool
@traced_tool("<tool>.<operation>")
def <operation>(<typed-args>) -> dict:
"""<One-line purpose> using deterministic synthetic data."""
span = trace.get_current_span()
span.set_attribute("zava.<tool>.<arg>", str(<arg>))
return _synth(<arg>)
def _synth(<arg>) -> dict:
"""Deterministic synthesis. Same input → same output across runs."""
seed = int(hashlib.sha256(str(<arg>).encode()).hexdigest()[:8], 16)
return {…}
class _<Operation>Params(BaseModel):
<arg>: <type> = Field(description="<one line>")
@define_tool(
name="<tool>_<operation>",
description=(
"<One sentence describing what this tool does and when an agent would call it. "
),
)
<tool>_<operation>_tool(params: _<Operation>Params) -> ToolResult:
:
result = <operation>(params.<arg>)
ToolResult(text_result_for_llm=json.dumps(result, ensure_ascii=))
Exception ex:
ToolResult(text_result_for_llm=, result_type=, error=(ex))
SDK contract. ToolResult.__init__ takes
text_result_for_llm: str and result_type: "success" | "failure".
It does NOT accept success= / content= (an older draft API).
Always JSON-serialise the dict result into text_result_for_llm; the
SDK feeds that string back to the model as the tool output. Returning
the wrong shape silently breaks every tool call — the model sees a
TypeError string and falls back to invented prose.
One Python module = one mcp_tool from the brief = one or more
operations. If the brief lists multiple operations for the same
mcp_tool, generate one <operation> function and one
<operation>_tool registration per operation, all in the same module.
Step 4 — Determinism
Every operation must be pure: same input → byte-identical output. Use
hashlib.sha256(str(input).encode()) to seed any synthetic structure
(employee names, calendar busy windows, leave balances). No time.time(),
no random.random(), no environment reads.
This matters because the meta-skill's quality bar is "two runs against
the same brief diff to nothing". Non-deterministic synthetic MCP adapters would break
that even if the SKILL.md generator is perfectly stable.
Step 5 — Synthetic-data realism
Make the synthesis good enough that the agent skill's allowed-tools
calls return something the agent can plausibly reason over.
For example, for a workday_hr_leave_balance.get_leave_balance(employee_id)
operation:
{
"employee_id": "EMP-0042",
"year": 2026,
"annual_entitlement_days": 25,
"taken_days": 12,
"booked_days": 5,
"remaining_days": 8,
"carry_over_from_prior_year": 0,
}
— numbers consistent (taken + booked + remaining = entitlement + carry_over),
keyed off the seed so the same employee_id always returns the same record.
Do not include real-looking PII. Synthesised employee names should follow
the existing repo convention (EMP-NNNN, fixture employees only).
Step 6 — Write the file
Write the assembled Python module to output_path. Create parent
directories if needed.
Step 7 — Self-check
Before returning, verify:
- Module imports resolve textually against the canonical examples
(
copilot.tools, opentelemetry.trace, pydantic, ._otel).
- Every
operation from the brief has both an <operation> function and
an <operation>_tool registration.
- Every
<operation> function is @traced_tool(…)-wrapped.
- The synth function is pure (no
time, random, os.environ,
network).
- No
TODO placeholder remains.
- The output JSON shape is internally consistent (cross-field
invariants hold).
If any check fails, fix in place. If a check fails because the brief
asks for something you can't deterministically synthesise, stop and tell
the caller.
Anti-patterns
- Reading from
data/synthetic/ to "make it more realistic". The real
synthetic corpus is for the existing two domains. New domains
synthesise from a seed.
- Importing
httpx and pointing at a non-existent localhost port. The
whole point of the in-memory synthetic MCP adapter is no port management.
- Inventing OTEL attribute names. Mirror what
claim_lookup.py does
(zava.<tool>.<arg>).
- Returning lists where the canonical examples return dicts. Stay
isomorphic.
- Adding multiple
mcp_tool files in one invocation. One file per call.
The caller decides how many calls.