John Ho commited on
Commit
c5df056
Β·
1 Parent(s): 9c917c1

recruited help from claude

Browse files
.claude/skills/gradio-mcp-tool-docstrings/SKILL.md ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: gradio-mcp-tool-docstrings
3
+ description: Write Gradio function docstrings that survive Gradio's MCP schema parser so the exposed tool is clear to LLM clients. Use this WHENEVER you write or edit a Gradio function that is (or will be) exposed over MCP, add mcp_server=True, set an api_name, or document a Gradio tool's description, parameters, or output schema β€” even if the user just says "make the tool description clearer" or "document the outputs". Especially load this before touching docstrings on functions wired into .click()/.submit()/gr.Interface in a Gradio app.
4
+ ---
5
+
6
+ # Gradio MCP tool docstrings
7
+
8
+ When a Gradio app runs with `mcp_server=True`, every API endpoint becomes an MCP tool, and the
9
+ tool's schema β€” the description an LLM reads, and the per-parameter docs β€” is built **from the
10
+ Python function's docstring and type hints**. The catch: Gradio's docstring parser
11
+ (`gradio/utils.py::get_function_description`) is primitive and *silently* mangles anything not
12
+ shaped exactly right. A docstring that reads beautifully to a human can still produce a broken,
13
+ truncated, or URL-stripped tool schema. This skill encodes what actually survives the parser, so
14
+ your tools are unambiguous to the model calling them.
15
+
16
+ The rules below were verified empirically against a live `/gradio_api/mcp/schema` endpoint β€” trust
17
+ them over how the docstring "looks."
18
+
19
+ ## How Gradio turns a function into an MCP tool
20
+
21
+ - **Tool name = the Python function name.** It is NOT the `api_name` you pass to `.click()` β€”
22
+ `api_name` only names the REST endpoint. So if you want the MCP tool called `detect_objects`,
23
+ **name the function `detect_objects`**. Renaming via `api_name` alone does nothing for MCP.
24
+ - **Tool description = the docstring prose *before* the `Args:` line.**
25
+ - **Per-parameter descriptions = the `Args:` section.**
26
+ - **Enable it:** `demo.launch(mcp_server=True)`. This needs **Gradio β‰₯ 5.28** and the
27
+ **`gradio[mcp]`** extra installed (on HF Spaces: bump `sdk_version` in `README.md` and add
28
+ `gradio[mcp]` to `requirements.txt`). On older Gradio, `mcp_server=True` is a no-op/error.
29
+
30
+ ## The parser's three rules (and the traps)
31
+
32
+ Internalize these three β€” most broken schemas come from violating rule 2 or 3.
33
+
34
+ 1. **Description block** (every line before `Args:`/`Returns:`): stripped and joined verbatim with
35
+ spaces. **Safe for anything** β€” URLs, colons, parentheses, multi-sentence prose. This is the
36
+ one place nothing gets mangled, so it is where the important content belongs.
37
+
38
+ 2. **`Args:` section**: each line is split on the first `:` into `name: description`. A wrapped
39
+ continuation line has no `:`, so **it is silently dropped**. Consequence: if a parameter
40
+ description spans two lines, the second line vanishes and you won't be warned.
41
+ β†’ **Keep every parameter description on a single line**, however long.
42
+
43
+ 3. **`Returns:` section**: each line is split on the first `:` and **everything before the colon is
44
+ discarded**. This quietly destroys machine-facing detail:
45
+ - a URL becomes garbage β€” `https://example.com` loses its `https:` and turns into
46
+ `//example.com`;
47
+ - field-labelled lines like `class_id (int): COCO index` lose the `class_id (int)` part.
48
+ β†’ **Do not put schema detail, field names, or URLs in `Returns:`.** Fold the output description
49
+ into the description-block prose instead.
50
+
51
+ ## Checklist for a rock-solid docstring
52
+
53
+ - Put the **full output schema in the description prose** (rule 1 keeps it intact), including any
54
+ reference URLs.
55
+ - **One line per `Args:` entry** (rule 2).
56
+ - **Skip or minimize `Returns:`** (rule 3) β€” it can't hold URLs or field names safely.
57
+ - **Name the function** exactly what you want the MCP tool called.
58
+ - Give a real **return type hint** (e.g. `-> tuple[Image.Image, list[dict]]`) for a cleaner schema.
59
+ - Make outputs **JSON-serializable** (see below).
60
+ - After editing, **verify against the live schema** (see Verification) β€” never trust appearances.
61
+
62
+ ## Coordinate / bounding-box outputs: always name the format
63
+
64
+ A bare number array like `[a, b, c, d]` is meaningless to a caller β€” the same four numbers mean
65
+ different boxes under different conventions. **Whenever a tool returns bounding boxes or any
66
+ coordinate/geometry, name the format explicitly in the description** and link a canonical
67
+ reference. Use the albumentations naming as the shared vocabulary:
68
+ https://albumentations.ai/docs/3-basic-usage/bounding-boxes-augmentations/#bounding-box-formats
69
+
70
+ Albumentations formats cheat-sheet β€” state which one you emit, and whether values are absolute
71
+ pixels or normalized 0–1:
72
+
73
+ | Format | Coordinates | Values |
74
+ |-----------------|------------------------------------------|-----------------|
75
+ | `pascal_voc` | `[x_min, y_min, x_max, y_max]` | absolute pixels |
76
+ | `albumentations`| `[x_min, y_min, x_max, y_max]` | normalized 0–1 |
77
+ | `coco` | `[x_min, y_min, width, height]` | absolute pixels |
78
+ | `yolo` | `[x_center, y_center, width, height]` | normalized 0–1 |
79
+ | `cxcywh` | `[x_center, y_center, width, height]` | absolute pixels |
80
+
81
+ Also state the **coordinate space** (which image the pixels are relative to). In this repo,
82
+ detections come from supervision `detections.xyxy` β†’ `[x_min, y_min, x_max, y_max]` in **absolute
83
+ pixels of the ORIGINAL input image** = albumentations **`pascal_voc`**. Say all of that; say what
84
+ it is NOT (not normalized, not `[x, y, width, height]`).
85
+
86
+ ## JSON-serializability
87
+
88
+ MCP structured output must be plain JSON. Detection/ML libraries hand back numpy scalars, which can
89
+ break serialization. Convert before returning: `int(...)` for indices/ids, `float(...)` for scores,
90
+ `.tolist()` for arrays. In this repo see `detect_and_annotate` in `app.py`.
91
+
92
+ ## Worked example (this repo's `detect_objects` in `app.py`)
93
+
94
+ Note: schema/URL/field detail lives in the prose; each `Args:` line is single-line; no `Returns:`.
95
+
96
+ ```python
97
+ def detect_objects(input_image, confidence, resolution, checkpoint) -> tuple[Image.Image, list[dict]]:
98
+ """Detect objects in an image using RF-DETR and return the annotated image plus structured detections.
99
+
100
+ RF-DETR ... It produces two outputs. Output 1 is the annotated image. Output 2 is a JSON list
101
+ of detections; each entry has: "class_id" (integer COCO index 0-79); "classname" (string);
102
+ "confidence" (float 0.0-1.0); and "bounding_box". The "bounding_box" is [x_min, y_min, x_max,
103
+ y_max] in ABSOLUTE PIXEL coordinates of the ORIGINAL input image (top-left & bottom-right
104
+ corners) -- the albumentations "pascal_voc" format, documented at
105
+ https://albumentations.ai/docs/3-basic-usage/bounding-boxes-augmentations/#bounding-box-formats .
106
+ NOT normalized to 0-1 and NOT [x, y, width, height].
107
+
108
+ Args:
109
+ input_image: The RGB image to run object detection on.
110
+ confidence: Minimum confidence score from 0.0 to 1.0; detections below this are discarded.
111
+ resolution: Square inference resolution in pixels; snapped to the backbone's multiple.
112
+ checkpoint: Which model to run: "base" (faster) or "large" (more accurate).
113
+ """
114
+ ```
115
+
116
+ ## Verification (do this after every docstring change)
117
+
118
+ 1. Run the app with `mcp_server=True` (startup logs should print an MCP SSE URL).
119
+ 2. Dump the live schema with the bundled script:
120
+ ```bash
121
+ python .claude/skills/gradio-mcp-tool-docstrings/scripts/dump_mcp_schema.py --port 7860
122
+ # or target one tool:
123
+ python .../scripts/dump_mcp_schema.py --tool detect_objects
124
+ ```
125
+ Confirm: the **tool name** is what you expect, the **description** is complete with any **URLs
126
+ intact**, and **every argument** has its full description (nothing truncated).
127
+ 3. **Isolated-stub technique** (no model / GPU needed): to iterate fast on parsing, replicate just
128
+ the function *signature + docstring* (a stub that returns dummy values) in a tiny app, wire it to
129
+ a button `.click(...)`, `demo.launch(mcp_server=True, prevent_thread_lock=True)`, then run the
130
+ dump script against it. This checks the docstring parsing in seconds without loading heavy deps.
131
+ Requires only `pip install "gradio[mcp]"`.
.claude/skills/gradio-mcp-tool-docstrings/scripts/dump_mcp_schema.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Dump the tool schema a running Gradio MCP server exposes.
3
+
4
+ Gradio builds its MCP tool schema from each API function's docstring + type hints, but its
5
+ docstring parser is primitive and silently mangles anything not shaped exactly right. The only
6
+ way to know what an MCP client actually sees is to read the live schema. This script fetches it
7
+ and prints, per tool: the name, the description, and each input argument's description -- so you
8
+ can confirm your docstring survived the parser intact (URLs present, arg descriptions complete,
9
+ tool named what you expect).
10
+
11
+ Prerequisite: launch the app with ``demo.launch(mcp_server=True)`` (needs gradio>=5.28 and the
12
+ ``gradio[mcp]`` extra). Then run this against the running server.
13
+
14
+ Usage:
15
+ python dump_mcp_schema.py # http://127.0.0.1:7860
16
+ python dump_mcp_schema.py --port 7860
17
+ python dump_mcp_schema.py --url http://127.0.0.1:7860
18
+ python dump_mcp_schema.py --tool detect_objects # only show one tool
19
+
20
+ Stdlib only -- no third-party deps, runs anywhere.
21
+ """
22
+ import argparse
23
+ import json
24
+ import sys
25
+ import urllib.error
26
+ import urllib.request
27
+
28
+
29
+ def fetch_schema(base_url: str, timeout: float = 15.0):
30
+ schema_url = base_url.rstrip("/") + "/gradio_api/mcp/schema"
31
+ try:
32
+ with urllib.request.urlopen(schema_url, timeout=timeout) as resp:
33
+ return json.loads(resp.read())
34
+ except urllib.error.URLError as e:
35
+ sys.exit(
36
+ f"ERROR: could not reach {schema_url}\n"
37
+ f" ({e})\n"
38
+ " Is the app running and launched with demo.launch(mcp_server=True)?\n"
39
+ " (mcp_server needs gradio>=5.28 and the gradio[mcp] extra installed.)"
40
+ )
41
+ except json.JSONDecodeError as e:
42
+ sys.exit(f"ERROR: {schema_url} did not return valid JSON: {e}")
43
+
44
+
45
+ def print_tool(tool: dict) -> None:
46
+ print("=" * 78)
47
+ print(f"TOOL: {tool.get('name', '<unnamed>')}")
48
+ print("-" * 78)
49
+ print("DESCRIPTION:")
50
+ print(f" {tool.get('description', '(none)')}")
51
+ props = (tool.get("inputSchema") or {}).get("properties") or {}
52
+ print("ARGUMENTS:")
53
+ if not props:
54
+ print(" (none)")
55
+ for name, spec in props.items():
56
+ desc = spec.get("description", "(no description)")
57
+ typ = spec.get("type", "?")
58
+ print(f" - {name} ({typ}): {desc}")
59
+ print()
60
+
61
+
62
+ def main() -> None:
63
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
64
+ parser.add_argument("--url", help="Base URL of the running Gradio app.")
65
+ parser.add_argument("--port", type=int, default=7860, help="Port (default 7860) if --url omitted.")
66
+ parser.add_argument("--host", default="127.0.0.1", help="Host (default 127.0.0.1) if --url omitted.")
67
+ parser.add_argument("--tool", help="Only print the tool whose name contains this string.")
68
+ parser.add_argument("--raw", action="store_true", help="Print the raw JSON schema instead of a summary.")
69
+ args = parser.parse_args()
70
+
71
+ base_url = args.url or f"http://{args.host}:{args.port}"
72
+ schema = fetch_schema(base_url)
73
+
74
+ if args.raw:
75
+ print(json.dumps(schema, indent=2))
76
+ return
77
+
78
+ tools = schema if isinstance(schema, list) else schema.get("tools", [])
79
+ if args.tool:
80
+ tools = [t for t in tools if args.tool in t.get("name", "")]
81
+ if not tools:
82
+ sys.exit(f"No tools found (filter={args.tool!r}). The app may expose no MCP tools.")
83
+
84
+ for tool in tools:
85
+ print_tool(tool)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
CLAUDE.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ A HuggingFace Space that exposes Moondream2's open-vocabulary detection (`point` and `object_detection`) via a Gradio interface. The entire app is `app.py` β€” a single `detect()` function wrapped by `gr.Interface`. README frontmatter (`title`, `sdk`, `sdk_version`, `app_file`) is the HF Space config and must stay valid YAML; the Space rebuilds from it.
8
+
9
+ ## Running
10
+
11
+ ```bash
12
+ pip install -r requirements.txt
13
+ python app.py
14
+ ```
15
+
16
+ Gradio launches with `mcp_server=True` (exposes an MCP endpoint) and `app_kwargs={"docs_url": "/docs"}` (attempt to expose FastAPI Swagger). Per the README, the `/docs` route is known not to work in practice β€” see https://github.com/gradio-app/gradio/issues/4054.
17
+
18
+ ## Runtime model
19
+
20
+ - Loads `vikhyatk/moondream2` at revision `2025-04-14` with `trust_remote_code=True`. Pinning the revision is intentional β€” the model repo changes its API surface across revisions, so bumping it can break `model.point(...)` / `model.detect(...)` return shapes.
21
+ - `@spaces.GPU(duration=30)` decorators are HF Spaces ZeroGPU directives β€” they only activate on the Space; locally they are no-ops requiring a CUDA device (the `device_map={"": "cuda"}` hard-codes GPU). Local CPU runs need that removed.
22
+ - `detect()` returns the inner list (`["points"]` or `["objects"]`) rather than the full dict β€” callers (including the MCP tool surface) depend on that shape.