Skip to content

Commit 840fa35

Browse files
committed
docs and guidance for self
1 parent 9458770 commit 840fa35

3 files changed

Lines changed: 289 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Agents working in this repo
2+
3+
Notes for AI assistants (Claude Code, Cursor, Continue, etc.) and humans pairing with them.
4+
5+
## Keep `tools do` aligned with `define`d actions
6+
7+
Each Ash domain in this repo (`DiffoExample.Access`, `DiffoExample.Nbn`) declares two parallel lists:
8+
9+
1. **`resources do ... resource X do define :name, action: :foo end end`** — the code-interface surface, generating `MyDomain.name/...` functions for Elixir callers.
10+
2. **`tools do tool :name, X, :foo end`** — the MCP surface, exposing the same actions to AI agents via [`ash_ai`](https://hexdocs.pm/ash_ai).
11+
12+
**When you add a new action to a resource, add it to BOTH places.** Forgetting to add the `tool` entry leaves the action invisible to MCP clients — the test suite won't catch it, the code compiles, the action works for Elixir callers, and the AI silently can't reach it.
13+
14+
The convention is:
15+
16+
- One `tool` entry per `define`-d action.
17+
- Tool name matches the code-interface name where possible (e.g. `define :build_cable``tool :build_cable, Cable, :build`). Disambiguate with a suffix where two resources have actions of the same name (e.g. `tool :assign_port_on_card` and `tool :assign_port_on_ntd`).
18+
- Read actions exposed as their natural read names (`tool :get_cable_by_id, Cable, :read`).
19+
20+
## Why the discipline
21+
22+
Tool-via-existing-action is the WWZD-shaped pattern: authorisation, validation, after-action choreography, multi-tenancy, polymorphism — all inherited from the action without writing AI-specific logic. The AI gets the same access an RSP-actor consumer has, no more and no less. That property only holds if every action the AI should be able to reach is declared as a tool.
23+
24+
## When NOT to add a tool
25+
26+
- **Internal-only actions** that no consumer (Elixir, HTTP, MCP) should call directly. These typically don't have a `define` either.
27+
- **Provider primitives** (`Diffo.Provider.create_party`, `Diffo.Provider.create_place`) — deliberately not exposed via Access/Nbn MCP. When Access/Nbn grow their own party/place relationship-management actions, expose those instead, and let authorisation gate what each actor can do.
28+
- **Duplicate `define`s wrapping the same action** — e.g. `define :get_rsp_by_epid, action: :read, get_by: :id` and `define :get_rsp_by_short_name, action: :read, get_by: :short_name` both wrap the `:read` action with different filters. One tool (`tool :get_rsp_by_epid, Rsp, :read`) covers both — the AI can supply the filter shape it needs via tool arguments. The code-interface defines are an Elixir convenience; the tool exposes the underlying action.
29+
30+
## Where this matters most
31+
32+
If you're modifying:
33+
34+
- A `*.ex` file under `lib/access/resources/`, `lib/access/services/`, or `lib/nbn/resources/` that adds/renames a `define` in its domain — also touch `lib/access/access.ex` or `lib/nbn/nbn.ex` and update the `tools do` block.
35+
- The `tools do` block itself — make sure the resource module is aliased at the top of the domain file.
36+
37+
## Quick check
38+
39+
After changes, count alignment:
40+
41+
```bash
42+
# tools declared
43+
grep -c "tool :" lib/access/access.ex lib/nbn/nbn.ex
44+
45+
# actions code-interface-defined
46+
grep -c "define :" lib/access/access.ex lib/nbn/nbn.ex
47+
```
48+
49+
The two should match (modulo intentional exclusions).

documentation/how_to/setup_mcp.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Setting up the diffo_example MCP server
2+
3+
This how-to walks through running the diffo_example MCP server locally and
4+
wiring AI clients (Claude Code, Claude Desktop, Cursor, custom) to it. The MCP
5+
surface exposes every action declared in the Access and Nbn domains' `tools do`
6+
blocks as a callable tool — 60 tools across the two domains as of writing.
7+
8+
See [issue #44](https://github.com/diffo-dev/diffo_example/issues/44) for the
9+
design context, and Zach's
10+
[Ash AI blog post](https://alembic.com.au/blog/ash-ai-comprehensive-llm-toolbox-for-ash-framework)
11+
for the framing.
12+
13+
## Prerequisites
14+
15+
- Elixir / Erlang installed (per `mix.exs` — currently `~> 1.18`).
16+
- Neo4j running locally with the credentials configured in `config/dev.exs`.
17+
- Dependencies fetched: `mix deps.get`.
18+
- Initial RSP data seeded on first start (handled automatically by
19+
`DiffoExample.Nbn.Initializer` when the app starts in dev).
20+
21+
## Starting the server
22+
23+
In a terminal in the project root:
24+
25+
```bash
26+
MIX_ENV=dev mix run --no-halt
27+
```
28+
29+
This boots the supervision tree, including `Plug.Cowboy` on port 4000. The MCP
30+
server is forwarded from `DiffoExample.Nbn.Router` at the path `/mcp`. The same
31+
Cowboy listener also serves the JSON:API routes and the `/catalog` endpoint.
32+
33+
You'll see Neo4j connection logs, then the listener-bound message. Leave it
34+
running.
35+
36+
## Verifying MCP is up
37+
38+
In a second terminal, send the three canonical MCP requests with `curl`.
39+
40+
### `initialize`
41+
42+
```bash
43+
curl -sS -X POST -H "Content-Type: application/json" \
44+
-d '{
45+
"jsonrpc":"2.0",
46+
"method":"initialize",
47+
"id":1,
48+
"params":{
49+
"protocolVersion":"2024-11-05",
50+
"capabilities":{},
51+
"clientInfo":{"name":"curl","version":"0"}
52+
}
53+
}' \
54+
http://localhost:4000/mcp
55+
```
56+
57+
Expected response shape:
58+
59+
```json
60+
{
61+
"id": 1,
62+
"jsonrpc": "2.0",
63+
"result": {
64+
"capabilities": {"tools": {"listChanged": false}},
65+
"protocolVersion": "2024-11-05",
66+
"serverInfo": {"name": "MCP Server", "version": "0.2.1"}
67+
}
68+
}
69+
```
70+
71+
### `tools/list`
72+
73+
```bash
74+
curl -sS -X POST -H "Content-Type: application/json" \
75+
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}' \
76+
http://localhost:4000/mcp
77+
```
78+
79+
Returns the full set of tools. Each tool entry includes `name`, `description`,
80+
and `inputSchema` (JSON Schema generated from the underlying Ash action's
81+
arguments). To count and peek at the first few names:
82+
83+
```bash
84+
curl -sS -X POST -H "Content-Type: application/json" \
85+
-d '{"jsonrpc":"2.0","method":"tools/list","id":2}' \
86+
http://localhost:4000/mcp |
87+
python3 -c '
88+
import json, sys
89+
d = json.load(sys.stdin)
90+
ts = d.get("result", {}).get("tools", [])
91+
print("count:", len(ts))
92+
for t in ts[:8]:
93+
print("-", t["name"])
94+
print("..." if len(ts) > 8 else "")'
95+
```
96+
97+
### `tools/call`
98+
99+
Try `list_rsps` — no-arg, returns the seeded RSPs:
100+
101+
```bash
102+
curl -sS -X POST -H "Content-Type: application/json" \
103+
-d '{
104+
"jsonrpc":"2.0",
105+
"method":"tools/call",
106+
"id":3,
107+
"params":{"name":"list_rsps","arguments":{}}
108+
}' \
109+
http://localhost:4000/mcp
110+
```
111+
112+
Should return a `result.content[0].text` containing the JSON-encoded list of
113+
six RSPs (Wedge-tail, Quokka, Ibis, Taipan, Echidna, Dugong).
114+
115+
## Wiring Claude Code
116+
117+
In any directory, with the server running:
118+
119+
```bash
120+
claude mcp add diffo --transport http http://localhost:4000/mcp
121+
```
122+
123+
This adds an entry to your global `~/.claude.json`. For a project-scoped
124+
config (writes to a `.mcp.json` next to the cwd):
125+
126+
```bash
127+
claude mcp add diffo --transport http -s project http://localhost:4000/mcp
128+
```
129+
130+
After adding, any Claude Code session can call the tools. Try prompts like:
131+
132+
- "list the RSPs"
133+
- "qualify a DSL service for a customer with this location and these parties"
134+
- "build a cable, define it with 60 pairs as copper, then auto-assign a pair to
135+
the qualified service"
136+
- "show me the path with id X and its assigned ports and cables"
137+
138+
Claude will discover the right tools via `tools/list`, call them with the
139+
arguments inferred from the prompt, and explain the results.
140+
141+
## Wiring Claude Desktop
142+
143+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
144+
or the equivalent on your OS:
145+
146+
```json
147+
{
148+
"mcpServers": {
149+
"diffo": {
150+
"type": "http",
151+
"url": "http://localhost:4000/mcp"
152+
}
153+
}
154+
}
155+
```
156+
157+
Restart Claude Desktop. The tools appear under the hammer icon in the prompt
158+
input area. Hover any tool to see its description and schema.
159+
160+
## Wiring Cursor / Continue / other MCP clients
161+
162+
Any MCP-aware editor or assistant follows the same shape. Point them at
163+
`http://localhost:4000/mcp` with HTTP transport. Refer to the client's MCP
164+
configuration docs for the exact file/UI path.
165+
166+
## What tools are available
167+
168+
The full list is discoverable via `tools/list` (see above). At a glance:
169+
170+
**Access (~23 tools)**
171+
- DslAccess: read, qualify, qualify_result, design_result
172+
- Shelf: read, build, define, relate, assign_slot
173+
- Card: read, build, define, relate, assign_port (`:assign_port_on_card`)
174+
- Cable: read, build, define, relate, assign_pair
175+
- Path: read, build, define, relate
176+
177+
**Nbn (~37 tools)**
178+
- NbnEthernet, Uni, Avc, Nni: read, build, define, relate (each)
179+
- Ntd: read, build, define, assign_port (`:assign_port_on_ntd`), relate
180+
- Cvc: read, build, define, assign_cvlan, relate
181+
- NniGroup: read, build, define, assign_svlan, relate
182+
- Rsp: inventory (`:list_rsps`), read, build, activate, suspend, deactivate
183+
184+
The tool name in MCP matches the `tools do` declaration in
185+
`lib/access/access.ex` and `lib/nbn/nbn.ex`.
186+
187+
## Adjusting the surface
188+
189+
The router is wired with `tools: true`, which exposes every tool declared on
190+
every domain in the configured `:diffo_example, :ash_domains` list. To narrow
191+
the surface, edit the `forward "/mcp"` block in `lib/nbn/router.ex` and
192+
replace `tools: true` with an explicit list of tool atoms — e.g.
193+
`tools: [:list_rsps, :get_path_by_id, :qualify_dsl]`.
194+
195+
For separate scopes (read-only vs full-surface, public vs authenticated), add
196+
a second `forward "/mcp_admin"` with its own tool list.
197+
198+
## Adding new tools
199+
200+
When you add a new action to a resource, add it to the appropriate domain's
201+
`tools do` block as well. See [AGENTS.md](../../AGENTS.md) for the
202+
keep-in-alignment convention. The compile won't catch a missing tool entry;
203+
the action will simply be invisible to MCP clients.
204+
205+
## Authorisation
206+
207+
The current router is unauthenticated — local-dev use case. Tools execute
208+
with no actor, which means:
209+
210+
- Resources with `bypass DiffoExample.Nbn.Checks.NoActor do authorize_if always() end`
211+
(every NBN resource — see `lib/nbn/rsp_ownership.ex`) execute as a
212+
bypass — Perentie-internal access.
213+
- Actions without that bypass policy may fail or behave differently with no
214+
actor.
215+
216+
For multi-tenant or production deployments, add
217+
`AshAuthentication.Strategy.ApiKey.Plug` (or similar) in a pipeline ahead of
218+
the MCP forward, and pass the resolved actor through to the tool calls. The
219+
existing RSP-actor multi-tenancy machinery will then bound what each MCP
220+
client can do based on its principal.
221+
222+
## Troubleshooting
223+
224+
- **`tools/list` returns count: 0** — the `forward "/mcp"` block isn't passing
225+
`tools: true` (or a tool list) and `otp_app: :diffo_example`. Check
226+
`lib/nbn/router.ex`.
227+
- **`Connection refused` on `http://localhost:4000/mcp`** — the server isn't
228+
running, or it crashed at startup. Restart with `MIX_ENV=dev mix run --no-halt`
229+
and watch the startup logs.
230+
- **A specific tool call errors with policy/auth message** — the action requires
231+
an actor that the unauthenticated MCP request can't supply. Either run with
232+
`MIX_ENV=test` (some test bypasses), use a tool that doesn't need an actor,
233+
or wire up auth as above.
234+
- **`tools/call` works in curl but Claude can't find the tool** — restart the
235+
Claude client after adding the MCP server. Many MCP clients only refresh
236+
the tools list on session start.
237+
- **Schema mismatches in tool args**`inputSchema` in `tools/list` is the
238+
source of truth. The Ash action's arguments (and their `public?` and types)
239+
determine the schema; private arguments aren't exposed.

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ defmodule DiffoExample.MixProject do
6363
"README.md": [title: "Guide"],
6464
"documentation/domains/diffo_example_nbn.livemd": [title: "NBN Livebook"],
6565
"documentation/domains/nbn.md": [title: "The NBN Domain"],
66+
"documentation/how_to/setup_mcp.md": [title: "Setup the MCP server"],
6667
"LICENSES/MIT.md": [title: "License"]
6768
],
6869
groups_for_extras: [

0 commit comments

Comments
 (0)