mcp-tools
GitHub定义如何创建、修改和审查 Operately MCP 工具。MCP 工具作为 API 的薄包装,负责声明元数据并适配输入。涵盖架构流程、OAuth 范围管理及目录更新,确保能力暴露给 ChatGPT/Claude 等客户端时的规范与安全。
Trigger Scenarios
Install
npx skills add operately/operately --skill mcp-tools -g -y
SKILL.md
Frontmatter
{
"name": "mcp-tools",
"description": "Defines how to add Operately MCP tools (API-first wrappers). Use when creating, changing, or reviewing MCP tools under app\/lib\/operately_web\/mcp\/tools\/, when the user mentions MCP tools, tool catalog, @expected_tool_names, or when exposing a new Operately capability to ChatGPT\/Claude MCP clients."
}
MCP Tools
MCP tools are thin wrappers over OperatelyWeb.Api.* handlers. API endpoints
are always the source of truth for auth, permissions, business logic, and
serialization. MCP tools only declare MCP-facing metadata (schemas, scopes,
examples) and adapt string-key JSON arguments into API inputs.
For templates, test skeletons, sort_order bands, and a full walkthrough, see reference.md.
Architecture
flowchart TD
start[User wants capability X] --> checkTool{MCP tool exists?}
checkTool -->|yes| stopDone[Stop or extend existing tool]
checkTool -->|no| checkApi{OperatelyWeb.Api handler exists?}
checkApi -->|yes| wrapOnly[Write MCP wrapper + MCP tests]
checkApi -->|no| addApi[Add API handler + register in api.ex]
addApi --> apiTests[Add API tests with TurboCase]
apiTests --> wrapOnly
wrapOnly --> catalog[Update tools_test.exs @expected_tool_names]
catalog --> done[Run targeted tests]
| Layer | Location | Responsibility |
|---|---|---|
| API (source of truth) | app/lib/operately_web/api/ |
Auth, permissions, business logic, serialization |
| API wrappers (when needed) | app/lib/operately_web/api/wrappers/ |
Adapt inputs MCP exposes differently (e.g. hub scope → resource_hub_id) |
| MCP tool | app/lib/operately_web/mcp/tools/ |
JSON schema, MCP metadata, string-key arg decoding, call API |
| Catalog | Registry (auto) + tools_test.exs (manual) |
Discovery and invariant tests |
Product rationale: specs/0012-operately-mcp.md § "Shared execution across surfaces".
OAuth scopes
MCP grants use two scopes. There is no mcp:destructive scope — destructive tools stay on
mcp:write, matching the CLI write-token model.
| Scope | safety_classification |
Tool access |
|---|---|---|
mcp:read |
:read_only |
Read-only tools |
mcp:write |
:write or :destructive |
All write and destructive tools |
Default scope on connect
Operately.Mcp.Resources.parse_scopes/1 defaults nil, empty string, or empty list to
mcp:read only. Clients that need write or destructive access must request mcp:write
explicitly in the OAuth scope parameter.
When authoring tools:
- Read-only →
required_scopes: ["mcp:read"],safety_classification: :read_only,read_annotations()(readOnlyHint: true) - Write →
required_scopes: ["mcp:write"],safety_classification: :write,write_annotations() - Destructive →
required_scopes: ["mcp:write"],safety_classification: :destructive,write_annotations()plusdestructiveHint: truein descriptors (client UX only — not a third scope)
Runtime enforcement in OperatelyWeb.McpController checks token scopes against each tool's
required_scopes before dispatch.
Scope
This skill covers adding, changing, and reviewing MCP tool wrappers.
Removing a tool requires deleting the module, its test, and its entry in
@expected_tool_names.
Workflow
Always follow these three steps before writing code.
1. Check if the MCP tool already exists
- Grep
app/lib/operately_web/mcp/tools/by domain and action name - Read
@expected_tool_namesinapp/test/operately/mcp/tools_test.exs— the canonical sorted catalog - Run the catalog test or inspect
OperatelyWeb.Mcp.Tools.list_definitions/0 - Do not add a duplicate tool with a slightly different name
If a tool already covers the capability, stop or extend it instead of creating a new one.
2. Check if an API endpoint already exists
- Search
app/lib/operately_web/api.exforquery(:…)/mutation(:…)registrations - Search
app/lib/operately_web/api/{domain}/for matching modules - Check
app/lib/operately_web/api/wrappers/for API module wrappers, which behave like usual endpoints - Read sibling MCP tools in the same domain — they show the expected API alias and input mapping
If the API exists and accepts the inputs MCP needs, skip to step 3 (wrapper only).
3. Create endpoint first, then MCP wrapper
When no suitable API exists, always API before MCP:
- Add API module under
app/lib/operately_web/api/{domain}/{action}.ex- Queries:
use TurboConnect.Query - Mutations:
use TurboConnect.Mutation+Actionpipeline
- Queries:
- Register in
app/lib/operately_web/api.exasquery(:…)ormutation(:…) - Add API tests under
app/test/operately_web/api/{domain}/{action}_test.exsusingOperatelyWeb.TurboCase— cover auth, validation, permissions - Add MCP wrapper under
app/lib/operately_web/mcp/tools/{domain}/{action}.ex - Add MCP unit test under
app/test/operately_web/mcp/tools/{domain}/{action}_test.exs - Update catalog test: add tool name to
@expected_tool_namesat the correct position (sort_order, then alphabetical tie-break)
Canonical example: list_task_statuses — API was added first
(app/lib/operately_web/api/tasks/list_task_statuses.ex +
app/test/operately_web/api/tasks/list_task_statuses_test.exs), then the MCP
wrapper (app/lib/operately_web/mcp/tools/tasks/list_task_statuses.ex +
app/test/operately_web/mcp/tools/tasks/list_task_statuses_test.exs).
MCP Tool Implementation
Module contract
use OperatelyWeb.Mcp.Tool- Module:
OperatelyWeb.Mcp.Tools.{Domain}.{PascalCase} - File:
app/lib/operately_web/mcp/tools/{domain}/{snake_case}.ex - Public tool
name:snake_caseverb phrase (stable MCP API) - Implement
definition/0andcall/2 - No manual registry edit —
OperatelyWeb.Mcp.Catalog.Registryauto-discovers compiled modules underOperatelyWeb.Mcp.Tools.*
definition/0 fields
| Field | Guidance |
|---|---|
company_mode |
:none, :authenticated, or :resource_derived — never accept company_id in input |
required_scopes + safety_classification |
Read → ["mcp:read"] + :read_only + read_annotations(); Write → ["mcp:write"] + :write + write_annotations() |
sort_order |
Catalog ordering band — see reference.md |
input_schema / output_schema |
JsonSchema.object/2; string keys; required array only for truly required args |
examples |
At least one realistic example with encoded IDs like "task_123" |
discovery_metadata |
%{"category" => "tasks"} etc. |
call/2 adaptation
- Arguments arrive as string-key maps; API inputs use atom keys
- Always decode IDs via
OperatelyWeb.Mcp.Helpers.decode_id/1/decode_optional_id/1— never raw UUID parsing - Return
{:ok, atom_key_map}matchingoutput_schematop-level keys, or{:error, :invalid_arguments | :not_found | :forbidden | …} - Use
{:error, :invalid_arguments}for bad client input (malformed IDs, conflicting scopes, invalid enums) - Optional field conventions are domain-specific — check sibling tools:
- Omit optional field to clear (e.g.
update_project_due_date: omitdue_date→ clear) - Use
Helpers.parse_day_date/1,markdown_to_rich_text/1,put_optional/3as appropriate
- Omit optional field to clear (e.g.
- Enums: validate against a whitelist before
String.to_existing_atom/1, or use explicit decode clauses (seeclose_project,create_link) - Hub scope: exactly one of
space_id,project_id,goal_idviaHelpers.decode_hub_scope/1; preferApi.Wrappers.DocsAndFiles.*for create/list operations
Do not call business logic directly from MCP — always go through API.
Testing
Two test layers — both required for new capabilities:
| Layer | File pattern | Framework | What to test |
|---|---|---|---|
| API (exhaustive) | app/test/operately_web/api/{domain}/{action}_test.exs |
OperatelyWeb.TurboCase |
Auth, validation, permissions, happy paths |
| MCP (adaptation) | app/test/operately_web/mcp/tools/{domain}/{action}_test.exs |
Operately.DataCase + ToolConnHelper |
Call ToolModule.call/2 directly; ID decoding; cross-company not_found; optional-field behavior |
MCP tests assume API correctness — do not re-test every permission matrix in MCP tests.
Catalog test (always update): add name to @expected_tool_names in
app/test/operately/mcp/tools_test.exs. Missing entry fails CI.
Run targeted tests:
make test FILE=app/test/operately_web/api/tasks/list_task_statuses_test.exs
make test FILE=app/test/operately_web/mcp/tools/tasks/list_task_statuses_test.exs
make test FILE=app/test/operately/mcp/tools_test.exs
Optional integration: app/test/operately_web/controllers/mcp_controller_test.exs
for end-to-end HTTP — only when adding novel transport/auth behavior.
Checklist
- Confirmed no existing MCP tool covers the capability
- API handler exists (or was added) and registered in
api.ex - API tests pass (
TurboCase) - MCP wrapper under
OperatelyWeb.Mcp.Tools.*with correct schemas, scopes,company_mode,sort_order - MCP unit tests pass (call
ToolModule.call/2viaToolConnHelper) -
@expected_tool_namesupdated intools_test.exs - Catalog test passes
- Optional fields / clear-on-omit behavior matches sibling tools in the domain
Version History
- 379b061 Current 2026-08-20 12:01


