Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/claude-code/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"m-dev-tools-mcp": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/m-dev-tools/m-dev-tools-mcp@main",
"m-dev-tools-mcp"
]
}
}
}
52 changes: 52 additions & 0 deletions examples/claude-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Claude Code integration

Drop-in MCP-server config for [Claude Code](https://docs.claude.com/en/docs/claude-code). Once the server is registered, Claude can route plain-English questions about the m-dev-tools org ("how do I parse JSON in M?") through `route_intent` instead of guessing from training data.

## Install

Two paths, both work:

### 1. uvx (from git) — what `.mcp.json` here uses

No release needed; pins to `main`. Picks up new merges on every server restart.

```bash
uvx --from git+https://github.com/m-dev-tools/m-dev-tools-mcp@main m-dev-tools-mcp
```

### 2. Release wheel (Track D onward)

Once `v0.1.0` ships:

```bash
pip install https://github.com/m-dev-tools/m-dev-tools-mcp/releases/download/v0.1.0/m_dev_tools_mcp-0.1.0-py3-none-any.whl
m-dev-tools-mcp # boot the stdio MCP server
```

Pin to a tag (`@v0.1.0`) in your `.mcp.json` when stability matters.

## Register with Claude Code

Copy `.mcp.json` to your project root (or merge with your existing one). Claude Code auto-discovers MCP servers from `.mcp.json` in the working directory.

Sanity check:

```bash
claude --print "list your MCP tools"
# expected: route_intent, describe, verify
```

## Other MCP clients

The `.mcp.json` shape here is portable. Codex / Continue / any MCP-capable agent should accept the same `{ mcpServers: { <name>: { command, args } } }` structure — refer to each client's docs for the config file location. Phase 4 ships Claude Code as the gating client; other clients are documented as "should work" but unverified (phase4-plan.md §9 risk note).

## Smoke test — agent-free

Don't want to open Claude Code? `smoke.sh` shells the MCP server's `--tool` CLI surface directly and asserts the canonical query (`"parse JSON in M"` → `module:m-stdlib#STDJSON`):

```bash
./smoke.sh
# → 0/1 exit; stdout contains "module:m-stdlib#STDJSON"
```

The same canonical query plus the recorded Claude Code session live in `session.md` (template, replace placeholders after you run it locally).
98 changes: 98 additions & 0 deletions examples/claude-code/session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Claude Code session transcript (template)

> **Status: TEMPLATE — needs to be filled in once with a real session.**
>
> Phase 4 Track C (per [phase4-plan.md §4 C3](https://github.com/m-dev-tools/.github/blob/main/docs/phase4-plan.md))
> calls for a recorded session that proves Claude Code routes the
> canonical question through this MCP server's `route_intent` tool —
> not by guessing from training data. The session can't be auto-
> recorded from inside Claude Code itself, so the steps below
> describe what to do. Replace the placeholder spans (`<<< … >>>`)
> with the real output once you've run the session locally, then
> commit the filled-in version.

## How to record a session

1. **Install Claude Code** if you haven't: <https://docs.claude.com/en/docs/claude-code/>.
2. **Register this MCP server.** Copy `examples/claude-code/.mcp.json` (sibling of this file) into the project root, or merge it into your existing `.mcp.json`.
3. **Open Claude Code** in this repo:

```bash
cd ~/m-dev-tools/m-dev-tools-mcp
claude
```

4. **Confirm the server is registered.** At the prompt:

```
list your MCP tools
```

You should see `route_intent`, `describe`, and `verify` in the response.

5. **Ask the canonical question:**

```
How do I parse JSON in M?
```

Claude should:

- Call `route_intent("parse JSON in M")` (visible in the session's tool-use trace).
- Receive `["module:m-stdlib#STDJSON"]`.
- Optionally call `describe("module:m-stdlib#STDJSON")` to follow the manifest URL pointer.
- Compose an answer that references `parse^STDJSON` from m-stdlib.

6. **Copy the session trace** (Claude Code's `--print` or the trace panel) into the section below, replacing the placeholder spans.

## Recorded session

**Date:** <<<2026-MM-DD>>>
**Claude Code version:** <<<output of `claude --version`>>>
**MCP server version:** <<<output of `m-dev-tools-mcp --version`>>>

### Tool list

```
<<< paste response to "list your MCP tools" >>>
```

### Canonical question

> How do I parse JSON in M?

### Tool-use trace

<details>
<summary>route_intent("parse JSON in M")</summary>

```json
<<< paste the route_intent response — should contain "module:m-stdlib#STDJSON" >>>
```

</details>

<details>
<summary>describe("module:m-stdlib#STDJSON") — if Claude followed the pointer</summary>

```json
<<< paste the describe response — should contain manifest_url and tool.repo >>>
```

</details>

### Final answer

```
<<< paste Claude's final answer to the user; it should reference parse^STDJSON >>>
```

### Verification

- [ ] `route_intent` was called (not Claude guessing from training).
- [ ] The response contained `module:m-stdlib#STDJSON`.
- [ ] The answer named `parse^STDJSON` (the actual m-stdlib symbol).

## Falling back to `smoke.sh`

For CI / scripted verification (no real Claude Code session), `smoke.sh` exits 0 when the same canonical query resolves through the MCP server's CLI surface. That's the always-on assertion; this session.md is the once-per-release human-eyes confirmation.
50 changes: 50 additions & 0 deletions examples/claude-code/smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Agent-free MCP-server smoke check. Per phase4-plan.md §4 C2:
# resolve the canonical "parse JSON in M" intent through the
# `route_intent` tool and confirm the typed ID lands in stdout.
#
# Default mode: pin to git main, install on demand via uvx. CI and
# local-dev users with the package already installed in a venv can
# point M_DEV_TOOLS_MCP_BIN at it to skip the uvx round-trip:
#
# M_DEV_TOOLS_MCP_BIN=$(pwd)/.venv/bin/m-dev-tools-mcp ./smoke.sh
#
# Exit codes:
# 0 — canonical query resolved (the typed ID was in the response)
# 1 — server emitted a response that did NOT include the typed ID
# 2 — the underlying CLI exited non-zero (network, install,
# structured DiscoveryError)

set -euo pipefail

QUERY="parse JSON in M"
EXPECTED='"module:m-stdlib#STDJSON"'

if [[ -n "${M_DEV_TOOLS_MCP_BIN:-}" ]]; then
CMD=("$M_DEV_TOOLS_MCP_BIN")
else
CMD=(uvx --from "git+https://github.com/m-dev-tools/m-dev-tools-mcp@main" m-dev-tools-mcp)
fi

echo "→ ${CMD[*]} --tool route_intent --query \"$QUERY\""

set +e
RESULT="$("${CMD[@]}" --tool route_intent --query "$QUERY")"
RC=$?
set -e

if [[ $RC -ne 0 ]]; then
echo "ERROR: CLI exited rc=$RC; response was:" >&2
echo "$RESULT" >&2
exit 2
fi

echo "$RESULT"

if grep -qF "$EXPECTED" <<<"$RESULT"; then
echo "✓ canonical query resolved to $EXPECTED"
exit 0
fi

echo "ERROR: response did not contain $EXPECTED" >&2
exit 1
117 changes: 109 additions & 8 deletions src/m_dev_tools_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,125 @@
"""Console entry point — boots the MCP server's stdio transport.
"""Console entry point.

Track B implements the three tool callbacks. Track A ships the entry
plumbing only: ``python -m m_dev_tools_mcp`` and the
``m-dev-tools-mcp`` console-script both land here.
Two modes:

* **MCP server mode (default)** — ``m-dev-tools-mcp`` with no args
boots the stdio MCP server. Each ``@server.tool()`` is exposed to
the connected MCP client. This is the path Claude Code / Codex /
Continue use.

* **CLI smoke mode** — ``m-dev-tools-mcp --tool route_intent --query
"…"`` runs one tool call out of process and prints the JSON
response on stdout. The smoke.sh under
``examples/claude-code/smoke.sh`` shells this surface so an
agent-free environment can verify the MCP server resolves the
canonical query.

Exit codes (CLI smoke mode):

* ``0`` — success
* ``2`` — usage error (unknown tool, missing required flag, etc.)
* ``3`` — :class:`DiscoveryError` from the tool itself; stdout
carries a JSON error blob ``{"error": true, "code": "...",
"message": "..."}`` so a shell script can switch on the code.

A missing ``--query`` / ``--typed-id`` / ``--repo`` is enforced
manually (argparse can't model "this flag is required only when
--tool=X" natively). The check happens after parsing so the error
message can name the missing flag directly.
"""

from __future__ import annotations

import argparse
import json
import sys
from typing import Any

from m_dev_tools_mcp import __version__
from m_dev_tools_mcp.server import (
DiscoveryError,
_describe_through_cache,
_route_intent_through_cache,
_verify_through_cache,
build_server,
)

_TOOL_CHOICES = ("route_intent", "describe", "verify")

from m_dev_tools_mcp.server import build_server

def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="m-dev-tools-mcp",
description=__doc__.splitlines()[0] if __doc__ else "",
exit_on_error=False,
)
parser.add_argument(
"-V",
"--version",
action="store_true",
help="Print the package version and exit.",
)
parser.add_argument(
"--tool",
choices=_TOOL_CHOICES,
help="Run a single tool call out of process and print its JSON response.",
)
parser.add_argument("--query", help="Query string for --tool route_intent.")
parser.add_argument("--typed-id", dest="typed_id", help="Typed ID for --tool describe.")
parser.add_argument("--repo", help="Repo slug or typed ID for --tool verify.")
return parser


def _run_tool(args: argparse.Namespace) -> int:
tool = args.tool
if tool == "route_intent":
if args.query is None:
print("error: --tool route_intent requires --query", file=sys.stderr)
return 2
result: Any = _route_intent_through_cache(args.query)
elif tool == "describe":
if args.typed_id is None:
print("error: --tool describe requires --typed-id", file=sys.stderr)
return 2
result = _describe_through_cache(args.typed_id)
elif tool == "verify":
if args.repo is None:
print("error: --tool verify requires --repo", file=sys.stderr)
return 2
result = _verify_through_cache(args.repo)
else: # pragma: no cover — argparse choices guard
print(f"error: unknown --tool {tool!r}", file=sys.stderr)
return 2

print(json.dumps(result, indent=2, sort_keys=False))
return 0


def main(argv: list[str] | None = None) -> int:
argv = sys.argv[1:] if argv is None else argv
if argv and argv[0] in {"--version", "-V"}:
from m_dev_tools_mcp import __version__
parser = _build_parser()
try:
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
except (argparse.ArgumentError, argparse.ArgumentTypeError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
except SystemExit as exc:
# exit_on_error=False covers most paths, but unknown --tool
# choices still trigger SystemExit. Translate into rc=2.
return int(exc.code) if isinstance(exc.code, int) else 2

if args.version:
print(__version__)
return 0

if args.tool is not None:
try:
return _run_tool(args)
except DiscoveryError as exc:
blob = {"error": True, "code": exc.code, "message": str(exc)}
print(json.dumps(blob, indent=2))
return 3

# Default: boot the MCP server's stdio transport.
server = build_server()
server.run()
return 0
Expand Down
Loading
Loading