下面把前面的tool_demo mcp改造成skill+cli的形式。
一、实现
1、代码

(1)store.py
业务数据mock
(2)cli.py
- 先解析根上的全局
-f/--json等,执行main(),写好ctx.obj。- 再解析下一个 token:必须是
user、school或skill。- 再解析该组下的命令名,例如
user list、user create ...。- 最后把剩余部分按各命令函数的参数声明解析成位置参数和选项。
no_args_is_help=True表示不带任何参数时只出帮助,不执行默认操作。
from __future__ import annotations
from pathlib import Path
import shutil
from typing import Any, Optional
import typer
from tools_cli_demo.output import emit
from tools_cli_demo.service.store import store
app = typer.Typer(add_completion=False, no_args_is_help=True)
def _json_flag(json_flag: bool, format_value: Optional[str]) -> bool:
if json_flag:
return True
if format_value is None:
return False
fmt = format_value.strip().lower()
if fmt in {"json", "j"}:
return True
if fmt in {"text", "t", "plain"}:
return False
raise typer.BadParameter("Unsupported --format (use: json|text)")
@app.callback()
def main(
ctx: typer.Context,
json: bool = typer.Option(False, "--json", "-j", help="Print JSON (alias of -f json)."),
format: Optional[str] = typer.Option(
None,
"--format",
"-f",
help='Output format: "json" or "text" (default: text).',
),
) -> None:
"""Demo CLI for tools_demo (MCP tools -> CLI commands)."""
ctx.ensure_object(dict)
ctx.obj["json"] = _json_flag(json, format)
user_app = typer.Typer(add_completion=False, no_args_is_help=True)
school_app = typer.Typer(add_completion=False, no_args_is_help=True)
skill_app = typer.Typer(add_completion=False, no_args_is_help=True)
app.add_typer(user_app, name="user")
app.add_typer(school_app, name="school")
app.add_typer(skill_app, name="skill")
@user_app.command("list")
def user_list(ctx: typer.Context) -> None:
"""List all users."""
rows = [u.model_dump(mode="json") for u in store.list_users()]
emit(data=rows, as_json=bool(ctx.obj["json"]))
@user_app.command("create")
def user_create(
ctx: typer.Context,
name: str = typer.Argument(..., help="User name (required)."),
email: Optional[str] = typer.Option(None, help="Email (optional)."),
school_id: Optional[str] = typer.Option(None, help="School id (optional)."),
) -> None:
"""Create a user."""
try:
u = store.create_user(name, email, school_id)
except ValueError as e:
payload: dict[str, Any] = {"ok": False, "detail": str(e)}
emit(data=payload, as_json=bool(ctx.obj["json"]))
raise typer.Exit(code=2) from e
emit(data=u.model_dump(mode="json"), as_json=bool(ctx.obj["json"]))
@school_app.command("list")
def school_list(ctx: typer.Context) -> None:
"""List all schools."""
rows = [s.model_dump(mode="json") for s in store.list_schools()]
emit(data=rows, as_json=bool(ctx.obj["json"]))
def _skill_md_path() -> Path:
# Repo layout:
# <repo>/SKILL.md
# <repo>/tools_cli_demo/cli.py
return Path(__file__).resolve().parents[1] / "SKILL.md"
@skill_app.command("path")
def skill_path(ctx: typer.Context) -> None:
"""Print absolute path to SKILL.md."""
p = str(_skill_md_path())
emit(data={"skill_md": p}, as_json=bool(ctx.obj["json"]))
@skill_app.command("install-cursor")
def skill_install_cursor(
ctx: typer.Context,
namespace: str = typer.Option("demo", help="Folder name under ~/.cursor/skills/"),
name: str = typer.Option("tools-demo", help="Skill folder name."),
force: bool = typer.Option(False, help="Overwrite if destination exists."),
) -> None:
"""Copy SKILL.md into ~/.cursor/skills/<namespace>/<name>/SKILL.md (demo helper)."""
src = _skill_md_path()
if not src.exists():
emit(data={"ok": False, "detail": f"missing SKILL.md: {src}"}, as_json=bool(ctx.obj["json"]))
raise typer.Exit(code=2)
home = Path.home()
dest_dir = home / ".cursor" / "skills" / namespace / name
dest_dir.mkdir(parents=True, exist_ok=True)
dest = dest_dir / "SKILL.md"
if dest.exists() and not force:
emit(
data={"ok": False, "detail": f"destination exists: {dest} (use --force)"},
as_json=bool(ctx.obj["json"]),
)
raise typer.Exit(code=2)
shutil.copy2(src, dest)
emit(
data={"ok": True, "installed_to": str(dest)},
as_json=bool(ctx.obj["json"]),
)
if __name__ == "__main__":
app()
(3)output.py
from __future__ import annotations
import json
from typing import Any
import typer
def emit(*, data: Any, as_json: bool) -> None:
if as_json:
typer.echo(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True))
return
if isinstance(data, dict) and data.get("ok") is False:
typer.secho(str(data.get("detail", data)), fg=typer.colors.RED, err=True)
raise typer.Exit(code=2)
typer.echo(str(data))
(4)skill.md
---
name: tools-demo
description: >-
tools_demo in-memory Users/Schools demo via the toolsdemo CLI (same behavior as MCP tools user_list, user_create, school_list).
Use when the user mentions tools_demo, tools_cli_demo, the tools_demo MCP server, FastMCP tools-demo-bridge, or the toolsdemo command.
Use when the user asks to list/create demo users, list schools, mirror MCP tool behavior without MCP, or install this SKILL into Cursor.
Prefer -f json for machine-readable output.
version: 0.1.0
---
# Tools demo (toolsdemo CLI)
CLI binary: `toolsdemo` (on PATH after `pip install -e .` in the repo root --- always invoke `toolsdemo` directly).
Execution pattern: `toolsdemo -f json <group> <command> [args...]`
---
## User operations
Equivalent MCP: `user_list`
```bash
toolsdemo -f json user list
```
Equivalent MCP: `user_create`
```bash
toolsdemo -f json user create "<name>" --email "<email>" --school-id "<school_id>"
```
| Argument | Required | Description |
| --- | --- | --- |
| `NAME` | Yes | User name |
| `--email` | No | Email |
| `--school-id` | No | Must exist in `school list` if set |
---
## School operations
Equivalent MCP: `school_list`
```bash
toolsdemo -f json school list
```
---
## Skill helpers (optional)
Print path to this repo's `SKILL.md`:
```bash
toolsdemo -f json skill path
```
Copy `SKILL.md` into Cursor skills dir (`~/.cursor/skills/demo/tools-demo/SKILL.md` by default):
```bash
toolsdemo -f json skill install-cursor
```
---
## Rules
- **When to load this skill:** user message references **`tools_demo`** / **`tools_cli_demo`**, the **`toolsdemo`** CLI, or MCP tools **`user_list`** / **`user_create`** / **`school_list`** for this demo. Do not use for unrelated user/school data systems.
- Always use `-f json` (or `--json`) when an agent parses output.
- Data is **in-memory** only; it resets when the CLI process exits.
- Full install and layout: see `README.md` in the repo.
2、cli安装
cd /d C:\python-project\tools_cli_demo
python -m venv .venv
.venv\Scripts\activate
pip install -e .

pip install -e .(装 Python 包)作用: 把
tools_cli_demo当成一个可安装的包,在系统里注册toolsdemo这条命令。
- 不装的话:一般要
python -m tools_cli_demo或写全路径才能跑。- 装了之后:在任意目录终端里直接敲
toolsdemo就能用user list、school list等子命令。
-e表示「可编辑安装」:你改仓库里的代码,不用反复重装,命令用的就是当前目录里的源码,让 Python 直接从你的源码树 import 这个包。
安装成功后,可以查看目录:


当然还有一种方法是把目录写进path。
3、cli使用
(1)cmd调用
打开一个新窗口
toolsdemo -f json school list

(2)ai coding调用
先把项目的skill.md copy到cursor中,我刚才的代码是使用curosr生成的,防止cursor直接查询代码,我有又copy到了codex的skill中
然后使用:
我本地有100多个skill,为了防止不命中,这里我直接指定名称了
