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

相关推荐
是Yu欸14 小时前
AI跨模态鉴伪技术实测
人工智能·安全·ai作画·aigc·合合信息·ai-native·waic
声网14 小时前
Vercel 发布 AI SDK 5,引入语音 API;Ollama 新版本支持多模态交互 丨日报
人工智能
蓝速科技14 小时前
蓝速科技 AI 双屏翻译机:私模工艺与 AB 麦算法实测解析
人工智能·科技
声网14 小时前
阿里小号停止续费,10 月底下架 App;音频技术公司 Bragi 联合 OpenAI 为第三方耳机引入 GPT 语音助手丨日报
人工智能
人民新视野14 小时前
AI数字人公司厂商让数字人导览讲解对话成为智慧展厅展馆标配
人工智能
liguojun202514 小时前
智慧文体旅系统选哪家?豆米跳跳十年技术沉淀支持源码交付
java·大数据·人工智能·物联网·1024程序员节
超自然祈祷14 小时前
科技在发展,我们所做工作的范式也在变——Software1.0、2.0、3.0...
人工智能·科技
大强同学14 小时前
Kimi Code CLI
人工智能·agent·claude·skill·kimi code cli
深度研习笔记14 小时前
OpenCV工业视觉实战10|违规语音播报+弹窗告警+重复告警过滤,打造智能声光预警系统(项目加分必备)
人工智能·opencv·计算机视觉
湘美书院--湘美谈教育14 小时前
湘美书院主理人:AI世代武侠小说的基本特征与蕴意
大数据·人工智能·深度学习·机器学习·生活