unity-api-mcp 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ # Override Unity install path (auto-detected if not set)
2
+ # UNITY_INSTALL_PATH=H:/Unity/6000.3.8f1
3
+
4
+ # Path to a Unity project for package source parsing (Input System, Addressables)
5
+ # UNITY_PROJECT_PATH=F:/Unity Projects/my-project
@@ -0,0 +1,8 @@
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ /data/
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: unity-api-mcp
3
+ Version: 1.0.0
4
+ Summary: MCP server providing ground-truth Unity 6 API documentation — prevents AI hallucination of signatures, namespaces, and deprecated APIs
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: mcp[cli]>=1.8.0
7
+ Requires-Dist: python-dotenv>=1.0
8
+ Provides-Extra: ingest
9
+ Requires-Dist: lxml>=5.0; extra == 'ingest'
@@ -0,0 +1,233 @@
1
+ # unity-api-mcp
2
+
3
+ Local MCP server that gives AI agents accurate Unity 6 API documentation. Prevents hallucinated method signatures, wrong namespaces, and deprecated API usage.
4
+
5
+ Works with Claude Code, Cursor, Windsurf, or any MCP-compatible AI tool.
6
+
7
+ ## What it does
8
+
9
+ 5 tools your AI can call:
10
+
11
+ | Tool | Purpose | Example |
12
+ |------|---------|---------|
13
+ | `search_unity_api` | Find APIs by keyword | "Tilemap SetTile", "async load scene" |
14
+ | `get_method_signature` | Get exact signatures with all overloads | `UnityEngine.Physics.Raycast` |
15
+ | `get_namespace` | Resolve `using` directives | "SceneManager" → `using UnityEngine.SceneManagement;` |
16
+ | `get_class_reference` | Full class reference card (all members) | "InputAction" → 31 methods/fields/properties |
17
+ | `get_deprecation_warnings` | Check if an API is obsolete + get replacement | "WWW" → Use UnityWebRequest instead |
18
+
19
+ **Coverage:** All UnityEngine/UnityEditor modules (~75K records), Input System, Addressables. 78K total records ship pre-built — no ingestion step required.
20
+
21
+ ## Requirements
22
+
23
+ - Python 3.10+
24
+ - That's it. The database ships pre-built. Unity does not need to be installed.
25
+
26
+ ## Setup
27
+
28
+ ### 1. Install
29
+
30
+ ```bash
31
+ pip install unity-api-mcp
32
+ ```
33
+
34
+ Or install from source:
35
+ ```bash
36
+ git clone <repo-url> unity-api-mcp
37
+ cd unity-api-mcp
38
+ pip install .
39
+ ```
40
+
41
+ ### 2. Add to your AI tool
42
+
43
+ #### Claude Code
44
+
45
+ Add to `~/.claude/mcp.json` (global) or `<project>/.mcp.json` (per-project):
46
+
47
+ **macOS / Linux:**
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "unity-api": {
52
+ "command": "unity-api-mcp",
53
+ "args": []
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ **Windows:**
60
+ ```json
61
+ {
62
+ "mcpServers": {
63
+ "unity-api": {
64
+ "command": "unity-api-mcp.exe",
65
+ "args": []
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ If the command isn't found (not on PATH), use the full path to the script:
72
+
73
+ **macOS / Linux:**
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "unity-api": {
78
+ "command": "/path/to/venv/bin/unity-api-mcp",
79
+ "args": []
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ **Windows:**
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "unity-api": {
90
+ "command": "C:\\path\\to\\venv\\Scripts\\unity-api-mcp.exe",
91
+ "args": []
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ #### Cursor / Windsurf
98
+
99
+ Add the same config to your MCP settings file. The server uses stdio transport (default).
100
+
101
+ ### 3. Verify it's working
102
+
103
+ Restart your AI tool (or run `/mcp` in Claude Code to reconnect), then ask:
104
+
105
+ *"What namespace does SceneManager belong to?"*
106
+
107
+ If everything is connected, it should call `get_namespace("SceneManager")` and answer `using UnityEngine.SceneManagement;`. If it guesses without calling a tool, the MCP server isn't connected — check the config paths and restart.
108
+
109
+ You can also try: *"Show me all methods on the Tilemap class"* — this should call `get_class_reference` and return 60+ methods.
110
+
111
+ ### 4. Tell your AI to use it proactively
112
+
113
+ Add the following to your project's `CLAUDE.md` (or equivalent instructions file). **This step is important** — without it, the AI has the tools but won't know when to reach for them.
114
+
115
+ ```markdown
116
+ ## Unity API Lookup (unity-api MCP)
117
+
118
+ Use the `unity-api` MCP tools to verify Unity API usage instead of guessing. **Do not hallucinate signatures.**
119
+
120
+ | When | Tool | Example |
121
+ |------|------|---------|
122
+ | Unsure about a method's parameters or return type | `get_method_signature` | `get_method_signature("UnityEngine.Tilemaps.Tilemap.SetTile")` |
123
+ | Need the `using` directive for a type | `get_namespace` | `get_namespace("SceneManager")` |
124
+ | Want to see all members on a class | `get_class_reference` | `get_class_reference("InputAction")` |
125
+ | Searching for an API by keyword | `search_unity_api` | `search_unity_api("async load scene")` |
126
+ | Checking if an API is deprecated | `get_deprecation_warnings` | `get_deprecation_warnings("FindObjectOfType")` |
127
+
128
+ **Rules:**
129
+ - Before writing a Unity API call you haven't used in this conversation, verify the signature with `get_method_signature`
130
+ - Before adding a `using` directive, verify with `get_namespace` if unsure
131
+ - Covers: all UnityEngine/UnityEditor modules, Input System, Addressables
132
+ - Does NOT cover: DOTween, VContainer, Newtonsoft.Json (third-party — rely on project source)
133
+ ```
134
+
135
+ ## AI-Assisted Setup
136
+
137
+ If an AI agent (Claude Code, etc.) is setting this up for you, give it these instructions:
138
+
139
+ > Read the README at `<path>/unity-api-mcp/README.md` and set up the MCP server for my Unity project at `<project-path>`.
140
+
141
+ The agent should:
142
+ 1. **Install** — `pip install unity-api-mcp` (or `pip install .` if cloned locally)
143
+ 2. **Find the executable** — run `which unity-api-mcp` (macOS/Linux) or `where unity-api-mcp` (Windows) to get the full path
144
+ 3. **Write MCP config** — add to `~/.claude/mcp.json` with `"command": "<full-path-to-unity-api-mcp>"`
145
+ 4. **Add CLAUDE.md instructions** — append the "Unity API Lookup" snippet from Step 4 above to the project's `CLAUDE.md`
146
+ 5. **Verify** — reconnect MCP (`/mcp` in Claude Code) and test: `get_namespace("SceneManager")` should return `using UnityEngine.SceneManagement;`
147
+
148
+ ## Advanced: Rebuild or extend the database
149
+
150
+ The shipped database covers Unity 6 (6000.x) engine APIs + Input System + Addressables. If you need to:
151
+ - **Update for a newer Unity version** — re-run ingestion
152
+ - **Add more packages** (e.g. Cinemachine, TextMeshPro) — re-run with `--project`
153
+ - **Match your exact package versions** — re-run with `--project`
154
+
155
+ ### Run ingestion
156
+
157
+ ```bash
158
+ # Activate venv first, then:
159
+
160
+ # Engine APIs only (requires Unity 6 installed)
161
+ python -m unity_api_mcp.ingest
162
+
163
+ # Engine APIs + packages from your project
164
+ python -m unity_api_mcp.ingest --project "/path/to/your/unity/project"
165
+ ```
166
+
167
+ If Unity isn't auto-detected, set the install path:
168
+
169
+ ```bash
170
+ # Windows (Command Prompt)
171
+ set UNITY_INSTALL_PATH=C:\Program Files\Unity\Hub\Editor\6000.3.8f1
172
+
173
+ # Windows (PowerShell)
174
+ $env:UNITY_INSTALL_PATH = "C:\Program Files\Unity\Hub\Editor\6000.3.8f1"
175
+
176
+ # macOS/Linux
177
+ export UNITY_INSTALL_PATH=/Applications/Unity/Hub/Editor/6000.3.8f1
178
+ ```
179
+
180
+ You can also add these to the MCP config `env` block so they're always available:
181
+
182
+ ```json
183
+ "env": {
184
+ "PYTHONPATH": "...",
185
+ "UNITY_INSTALL_PATH": "...",
186
+ "UNITY_PROJECT_PATH": "..."
187
+ }
188
+ ```
189
+
190
+ ### What ingestion parses
191
+
192
+ **XML IntelliSense files** — Ship with every Unity installation at `Editor/Data/Managed/`. 139 XML files covering all UnityEngine and UnityEditor modules.
193
+
194
+ **C# source doc comments** — Unity packages (Input System, Addressables, etc.) ship as source code in your project's `Library/PackageCache/`. The ingestion pipeline parses `///` XML doc comments from `.cs` files.
195
+
196
+ ## Project structure
197
+
198
+ ```
199
+ unity-api-mcp/
200
+ ├── src/unity_api_mcp/
201
+ │ ├── server.py # MCP server — 5 tools
202
+ │ ├── db.py # SQLite + FTS5 database layer
203
+ │ ├── xml_parser.py # Parse Unity XML IntelliSense files
204
+ │ ├── cs_doc_parser.py # Parse C# doc comments from package source
205
+ │ ├── unity_paths.py # Locate Unity install + package dirs
206
+ │ ├── ingest.py # CLI ingestion pipeline
207
+ │ └── data/
208
+ │ └── unity_docs.db # Pre-built SQLite database (78K records, ships with package)
209
+ ├── pyproject.toml
210
+ └── .env.example
211
+ ```
212
+
213
+ ## Troubleshooting
214
+
215
+ **"No results found" for a query**
216
+ - The pre-built database should be included in the package. If missing, reinstall: `pip install --force-reinstall unity-api-mcp`
217
+ - Or re-run ingestion to rebuild (see Advanced section)
218
+
219
+ **Server won't start**
220
+ - Check Python version: `python --version` (needs 3.10+)
221
+ - Check the command path: run `which unity-api-mcp` (macOS/Linux) or `where unity-api-mcp` (Windows)
222
+ - If not found, use the full path in your MCP config
223
+
224
+ **Third-party packages return no results**
225
+ - DOTween, VContainer, Newtonsoft.Json are not indexed (third-party, not Unity packages)
226
+ - Only Unity first-party packages are supported via ingestion with `--project`
227
+
228
+ **Want to add more packages**
229
+ - Run `python -m unity_api_mcp.ingest --project "/path/to/project"` to parse all Unity packages in your project's `Library/PackageCache/`
230
+
231
+ ## License
232
+
233
+ MIT
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "unity-api-mcp"
7
+ version = "1.0.0"
8
+ description = "MCP server providing ground-truth Unity 6 API documentation — prevents AI hallucination of signatures, namespaces, and deprecated APIs"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "mcp[cli]>=1.8.0",
12
+ "python-dotenv>=1.0",
13
+ ]
14
+
15
+ [project.optional-dependencies]
16
+ ingest = [
17
+ "lxml>=5.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ unity-api-mcp = "unity_api_mcp.server:main"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/unity_api_mcp"]
25
+ exclude = ["*.db-shm", "*.db-wal"]
26
+
27
+ [tool.hatch.build.targets.wheel.force-include]
28
+ "src/unity_api_mcp/data/unity_docs.db" = "unity_api_mcp/data/unity_docs.db"
File without changes
@@ -0,0 +1,306 @@
1
+ """Parse C# XML doc comments (///) from source files into structured records.
2
+
3
+ Used for Unity packages that ship as source code (Input System, Addressables)
4
+ rather than pre-built DLLs with XML IntelliSense files.
5
+ """
6
+
7
+ import re
8
+ from pathlib import Path
9
+
10
+
11
+ # Match /// comment blocks and the declaration that follows
12
+ _DOC_COMMENT_LINE = re.compile(r"^\s*///\s?(.*)")
13
+ _NAMESPACE_RE = re.compile(r"^\s*namespace\s+([\w.]+)")
14
+ _CLASS_RE = re.compile(
15
+ r"^\s*(?:public|internal|private|protected)?\s*(?:static\s+)?(?:abstract\s+)?(?:sealed\s+)?(?:partial\s+)?"
16
+ r"(?:class|struct|interface|enum)\s+(\w+)"
17
+ )
18
+ _METHOD_RE = re.compile(
19
+ r"^\s*(?:public|internal|protected)\s+(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:abstract\s+)?(?:new\s+)?"
20
+ r"(?:async\s+)?(?:[\w<>\[\],\s?]+?)\s+(\w+)\s*(?:<[^>]+>)?\s*\("
21
+ )
22
+ _PROPERTY_RE = re.compile(
23
+ r"^\s*(?:public|internal|protected)\s+(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:abstract\s+)?(?:new\s+)?"
24
+ r"([\w<>\[\],\s?]+?)\s+(\w+)\s*\{"
25
+ )
26
+ _FIELD_RE = re.compile(
27
+ r"^\s*(?:public|internal|protected)\s+(?:static\s+)?(?:readonly\s+)?(?:const\s+)?"
28
+ r"([\w<>\[\],\s?]+?)\s+(\w+)\s*[;=]"
29
+ )
30
+ _EVENT_RE = re.compile(
31
+ r"^\s*(?:public|internal|protected)\s+(?:static\s+)?event\s+"
32
+ r"([\w<>\[\],\s?]+?)\s+(\w+)\s*[;{]"
33
+ )
34
+ _PARAM_RE = re.compile(r"<param\s+name=[\"'](\w+)[\"']>(.*?)</param>")
35
+ _RETURNS_RE = re.compile(r"<returns>(.*?)</returns>")
36
+ _SUMMARY_RE = re.compile(r"<summary>(.*?)</summary>", re.DOTALL)
37
+ _SEE_CREF_RE = re.compile(r"<see\s+cref=[\"']([^\"']+)[\"']\s*/>")
38
+ _XML_TAG_RE = re.compile(r"<[^>]+>")
39
+
40
+ _DEPRECATED_PATTERNS = re.compile(
41
+ r"\b(obsolete|deprecated)\b|use\s+\S+\s+instead",
42
+ re.IGNORECASE,
43
+ )
44
+
45
+
46
+ def parse_cs_directory(directory: Path) -> list[dict]:
47
+ """Recursively parse all .cs files in a directory for XML doc comments."""
48
+ records = []
49
+ cs_files = list(directory.rglob("*.cs"))
50
+
51
+ for cs_file in cs_files:
52
+ # Skip test files, samples, editor-only
53
+ rel = str(cs_file.relative_to(directory))
54
+ if any(skip in rel.lower() for skip in ("test", "sample", "example", "doccode")):
55
+ continue
56
+
57
+ try:
58
+ file_records = _parse_cs_file(cs_file)
59
+ records.extend(file_records)
60
+ except Exception:
61
+ continue
62
+
63
+ return records
64
+
65
+
66
+ def _parse_cs_file(path: Path) -> list[dict]:
67
+ """Parse a single .cs file and extract documented members."""
68
+ try:
69
+ text = path.read_text(encoding="utf-8-sig", errors="replace")
70
+ except (OSError, UnicodeDecodeError):
71
+ return []
72
+
73
+ lines = text.splitlines()
74
+ records = []
75
+ current_namespace = ""
76
+ class_stack = [] # Stack of class names for nesting
77
+
78
+ i = 0
79
+ while i < len(lines):
80
+ line = lines[i]
81
+
82
+ # Track namespace
83
+ ns_match = _NAMESPACE_RE.match(line)
84
+ if ns_match:
85
+ current_namespace = ns_match.group(1)
86
+ i += 1
87
+ continue
88
+
89
+ # Track class/struct/interface nesting
90
+ cls_match = _CLASS_RE.match(line)
91
+ if cls_match and not _is_doc_comment(line):
92
+ # Check if there's a doc comment block above
93
+ doc_block, params, returns_text = _extract_doc_block(lines, i)
94
+ class_name = cls_match.group(1)
95
+ class_stack.append(class_name)
96
+
97
+ if doc_block:
98
+ fqn = f"{current_namespace}.{class_name}" if current_namespace else class_name
99
+ summary = _clean_xml_text(doc_block)
100
+ deprecated = bool(_DEPRECATED_PATTERNS.search(summary))
101
+ records.append({
102
+ "fqn": fqn,
103
+ "namespace": current_namespace,
104
+ "class_name": class_name,
105
+ "member_name": "",
106
+ "member_type": "type",
107
+ "summary": summary,
108
+ "params_json": [],
109
+ "returns_text": "",
110
+ "deprecated": deprecated,
111
+ "deprecation_hint": _extract_deprecation_hint(summary) if deprecated else "",
112
+ })
113
+
114
+ i += 1
115
+ continue
116
+
117
+ # Track scope for class stack (simplified brace counting)
118
+ if "}" in line and not "//" in line.split("}")[0]:
119
+ # This is a rough heuristic — good enough for top-level classes
120
+ pass
121
+
122
+ # Check for doc comment block
123
+ if _is_doc_comment(line):
124
+ # Collect the full doc block
125
+ doc_start = i
126
+ while i < len(lines) and _is_doc_comment(lines[i]):
127
+ i += 1
128
+
129
+ # Now lines[i] should be the declaration
130
+ if i < len(lines):
131
+ decl_line = lines[i]
132
+ doc_text = "\n".join(
133
+ _DOC_COMMENT_LINE.match(lines[j]).group(1)
134
+ for j in range(doc_start, i)
135
+ if _DOC_COMMENT_LINE.match(lines[j])
136
+ )
137
+
138
+ record = _parse_declaration(
139
+ decl_line, doc_text, current_namespace,
140
+ class_stack[-1] if class_stack else ""
141
+ )
142
+ if record:
143
+ records.append(record)
144
+ continue
145
+
146
+ i += 1
147
+
148
+ return records
149
+
150
+
151
+ def _is_doc_comment(line: str) -> bool:
152
+ stripped = line.strip()
153
+ return stripped.startswith("///")
154
+
155
+
156
+ def _extract_doc_block(lines: list[str], decl_index: int) -> tuple[str, list[dict], str]:
157
+ """Look backwards from a declaration to find its doc comment block."""
158
+ doc_lines = []
159
+ i = decl_index - 1
160
+ while i >= 0 and _is_doc_comment(lines[i]):
161
+ match = _DOC_COMMENT_LINE.match(lines[i])
162
+ if match:
163
+ doc_lines.insert(0, match.group(1))
164
+ i -= 1
165
+
166
+ if not doc_lines:
167
+ return "", [], ""
168
+
169
+ doc_text = "\n".join(doc_lines)
170
+
171
+ # Extract summary
172
+ summary_match = _SUMMARY_RE.search(doc_text)
173
+ summary = summary_match.group(1).strip() if summary_match else doc_text
174
+
175
+ # Extract params
176
+ params = [
177
+ {"name": m.group(1), "description": _clean_xml_text(m.group(2))}
178
+ for m in _PARAM_RE.finditer(doc_text)
179
+ ]
180
+
181
+ # Extract returns
182
+ returns_match = _RETURNS_RE.search(doc_text)
183
+ returns_text = _clean_xml_text(returns_match.group(1)) if returns_match else ""
184
+
185
+ return summary, params, returns_text
186
+
187
+
188
+ def _parse_declaration(decl_line: str, doc_text: str,
189
+ namespace: str, class_name: str) -> dict | None:
190
+ """Parse a C# declaration line and combine with doc text into a record."""
191
+
192
+ # Extract summary
193
+ summary_match = _SUMMARY_RE.search(doc_text)
194
+ summary = _clean_xml_text(summary_match.group(1).strip() if summary_match else doc_text)
195
+
196
+ if not summary or len(summary) < 3:
197
+ return None
198
+
199
+ # Extract params
200
+ params = [
201
+ {"name": m.group(1), "description": _clean_xml_text(m.group(2))}
202
+ for m in _PARAM_RE.finditer(doc_text)
203
+ ]
204
+
205
+ # Extract returns
206
+ returns_match = _RETURNS_RE.search(doc_text)
207
+ returns_text = _clean_xml_text(returns_match.group(1)) if returns_match else ""
208
+
209
+ # Detect deprecation
210
+ deprecated = bool(_DEPRECATED_PATTERNS.search(summary))
211
+ deprecation_hint = _extract_deprecation_hint(summary) if deprecated else ""
212
+
213
+ # Try to match declaration type
214
+ # Check class/struct/interface first
215
+ cls_match = _CLASS_RE.match(decl_line)
216
+ if cls_match:
217
+ member_name_str = cls_match.group(1)
218
+ fqn = f"{namespace}.{member_name_str}" if namespace else member_name_str
219
+ return {
220
+ "fqn": fqn,
221
+ "namespace": namespace,
222
+ "class_name": member_name_str,
223
+ "member_name": "",
224
+ "member_type": "type",
225
+ "summary": summary,
226
+ "params_json": params,
227
+ "returns_text": returns_text,
228
+ "deprecated": deprecated,
229
+ "deprecation_hint": deprecation_hint,
230
+ }
231
+
232
+ if not class_name:
233
+ return None
234
+
235
+ # Event (check before property — similar syntax)
236
+ event_match = _EVENT_RE.match(decl_line)
237
+ if event_match:
238
+ member_name_str = event_match.group(2)
239
+ fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
240
+ return _build_record(fqn, namespace, class_name, member_name_str, "event",
241
+ summary, params, returns_text, deprecated, deprecation_hint)
242
+
243
+ # Method
244
+ method_match = _METHOD_RE.match(decl_line)
245
+ if method_match:
246
+ member_name_str = method_match.group(1)
247
+ # Skip property accessors
248
+ if member_name_str in ("get", "set", "add", "remove"):
249
+ return None
250
+ fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
251
+ return _build_record(fqn, namespace, class_name, member_name_str, "method",
252
+ summary, params, returns_text, deprecated, deprecation_hint)
253
+
254
+ # Property
255
+ prop_match = _PROPERTY_RE.match(decl_line)
256
+ if prop_match:
257
+ member_name_str = prop_match.group(2)
258
+ # Skip common false positives
259
+ if member_name_str in ("class", "struct", "interface", "enum", "namespace",
260
+ "if", "else", "for", "while", "switch", "try", "catch"):
261
+ return None
262
+ fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
263
+ return _build_record(fqn, namespace, class_name, member_name_str, "property",
264
+ summary, params, returns_text, deprecated, deprecation_hint)
265
+
266
+ # Field
267
+ field_match = _FIELD_RE.match(decl_line)
268
+ if field_match:
269
+ member_name_str = field_match.group(2)
270
+ fqn = f"{namespace}.{class_name}.{member_name_str}" if namespace else f"{class_name}.{member_name_str}"
271
+ return _build_record(fqn, namespace, class_name, member_name_str, "field",
272
+ summary, params, returns_text, deprecated, deprecation_hint)
273
+
274
+ return None
275
+
276
+
277
+ def _build_record(fqn, namespace, class_name, member_name, member_type,
278
+ summary, params, returns_text, deprecated, deprecation_hint):
279
+ return {
280
+ "fqn": fqn,
281
+ "namespace": namespace,
282
+ "class_name": class_name,
283
+ "member_name": member_name,
284
+ "member_type": member_type,
285
+ "summary": summary,
286
+ "params_json": params,
287
+ "returns_text": returns_text,
288
+ "deprecated": deprecated,
289
+ "deprecation_hint": deprecation_hint,
290
+ }
291
+
292
+
293
+ def _clean_xml_text(text: str) -> str:
294
+ """Strip XML tags and normalize whitespace."""
295
+ # Replace <see cref="Foo"/> with Foo
296
+ text = _SEE_CREF_RE.sub(lambda m: m.group(1).split(".")[-1], text)
297
+ # Strip remaining XML tags
298
+ text = _XML_TAG_RE.sub("", text)
299
+ # Normalize whitespace
300
+ text = re.sub(r"\s+", " ", text).strip()
301
+ return text
302
+
303
+
304
+ def _extract_deprecation_hint(summary: str) -> str:
305
+ match = re.search(r"(?:use|Use)\s+(\S+?)[\s.]", summary)
306
+ return match.group(1) if match else ""