python AI工程(二)python实现skill+cli

下面把前面的tool_demo mcp改造成skill+cli的形式。

一、实现

1、代码

(1)store.py

业务数据mock

(2)cli.py

  1. 先解析根上的全局 -f / --json 等,执行 main(),写好 ctx.obj
  2. 再解析下一个 token:必须是 userschoolskill
  3. 再解析该组下的命令名,例如 user listuser create ...
  4. 最后把剩余部分按各命令函数的参数声明解析成位置参数和选项。

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 listschool 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,为了防止不命中,这里我直接指定名称了

相关推荐
朝新_1 小时前
【Spring AI 】核心知识体系梳理:从入门到实战
java·人工智能·spring
人工智能AI技术1 小时前
C#调用大模型
人工智能
陈老老老板1 小时前
Bright Data Web Scraper 实战:构建 eBay Web Scraping 自动化 Skill(2026)
大数据·人工智能·自动化
霍小毛2 小时前
轻量黑科技!数字孪生重构智慧城乡供水一体化新范式
人工智能
如此风景2 小时前
Playwright 速查表
人工智能
草青工作室2 小时前
AI主流大模型参数量和收费情况参考(26年4月)
人工智能
Y敲键盘的地方2 小时前
第1章:从命令行到智能体
人工智能
雷工笔记2 小时前
读书笔记|《AI知识库:个人与企业的智慧玩法》
人工智能
李可以量化2 小时前
【2026 量化工具选型】通达信 TdxQuant vs 迅投 QMT/miniQMT 深度对比:新手该怎么选?
大数据·人工智能·区块链·通达信·qmt·量化 qmt ptrade