fastmcp-builder 0.3.0__py3-none-any.whl

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.
Files changed (50) hide show
  1. fastmcp_builder-0.3.0.dist-info/METADATA +125 -0
  2. fastmcp_builder-0.3.0.dist-info/RECORD +50 -0
  3. fastmcp_builder-0.3.0.dist-info/WHEEL +4 -0
  4. fastmcp_builder-0.3.0.dist-info/entry_points.txt +4 -0
  5. mcpforge/__init__.py +23 -0
  6. mcpforge/api_client.py +158 -0
  7. mcpforge/cli.py +906 -0
  8. mcpforge/discovery.py +64 -0
  9. mcpforge/doctor.py +101 -0
  10. mcpforge/generator.py +64 -0
  11. mcpforge/generator_ts.py +36 -0
  12. mcpforge/inspection.py +145 -0
  13. mcpforge/mcp_server.py +247 -0
  14. mcpforge/models.py +189 -0
  15. mcpforge/openapi.py +243 -0
  16. mcpforge/planner.py +58 -0
  17. mcpforge/prompts/__init__.py +23 -0
  18. mcpforge/prompts/generator.md +180 -0
  19. mcpforge/prompts/generator_multi.md +59 -0
  20. mcpforge/prompts/generator_ts.md +49 -0
  21. mcpforge/prompts/planner.md +152 -0
  22. mcpforge/prompts/self_heal.md +24 -0
  23. mcpforge/prompts/test_gen.md +82 -0
  24. mcpforge/prompts/test_gen_ts.md +44 -0
  25. mcpforge/prompts/updater.md +15 -0
  26. mcpforge/providers.py +112 -0
  27. mcpforge/py.typed +0 -0
  28. mcpforge/sandbox.py +69 -0
  29. mcpforge/security.py +160 -0
  30. mcpforge/self_heal.py +204 -0
  31. mcpforge/template_hints.py +50 -0
  32. mcpforge/templates/README.md.j2 +139 -0
  33. mcpforge/templates/config.json.j2 +13 -0
  34. mcpforge/templates/env.example.j2 +3 -0
  35. mcpforge/templates/fastmcp.json.j2 +27 -0
  36. mcpforge/templates/init_server.py.j2 +15 -0
  37. mcpforge/templates/init_test.py.j2 +11 -0
  38. mcpforge/templates/pyproject.toml.j2 +18 -0
  39. mcpforge/templates/ts/README.md.j2 +44 -0
  40. mcpforge/templates/ts/config.json.j2 +8 -0
  41. mcpforge/templates/ts/env.example.j2 +3 -0
  42. mcpforge/templates/ts/gitignore.j2 +4 -0
  43. mcpforge/templates/ts/package.json.j2 +23 -0
  44. mcpforge/templates/ts/tsconfig.json.j2 +15 -0
  45. mcpforge/test_generator.py +27 -0
  46. mcpforge/updater.py +56 -0
  47. mcpforge/utils.py +11 -0
  48. mcpforge/validator.py +310 -0
  49. mcpforge/validator_ts.py +139 -0
  50. mcpforge/writer.py +206 -0
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.3
2
+ Name: fastmcp-builder
3
+ Version: 0.3.0
4
+ Summary: Generate complete, runnable FastMCP 3.x MCP servers from plain-English descriptions
5
+ Keywords: mcp,fastmcp,anthropic,claude,code-generation,llm
6
+ Author: saagpatel
7
+ Author-email: saagpatel <saagarpatel08@gmail.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Code Generators
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Requires-Dist: anthropic>=0.87.0
17
+ Requires-Dist: click>=8.1.0
18
+ Requires-Dist: fastmcp>=3.2.3
19
+ Requires-Dist: jinja2>=3.1.6
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: rich>=15.0.0
22
+ Requires-Dist: pyyaml>=6.0.3
23
+ Requires-Python: >=3.12
24
+ Project-URL: Homepage, https://github.com/saagpatel/mcpforge
25
+ Project-URL: Repository, https://github.com/saagpatel/mcpforge
26
+ Project-URL: Bug Tracker, https://github.com/saagpatel/mcpforge/issues
27
+ Description-Content-Type: text/markdown
28
+
29
+ # mcpforge
30
+
31
+ [![Python](https://img.shields.io/badge/Python-3776ab?style=flat-square&logo=python)](#) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](#)
32
+
33
+ > One sentence. One command. A complete MCP server, ready to run.
34
+
35
+ mcpforge generates production-ready FastMCP 3.x MCP servers from plain-English descriptions. You describe what you want; it produces tools, input validation, error handling, a pytest test suite, run configuration, and client setup docs — all wired together and ready to inspect, validate, and install.
36
+
37
+ ## Features
38
+
39
+ - **Plain-English generation** — describe your server in natural language; Claude writes the implementation
40
+ - **Complete project scaffold** — tools, Pydantic input models, error handling, `pyproject.toml`, and a pytest suite generated together
41
+ - **FastMCP 3.x native** — output uses modern FastMCP decorators and transport configuration, not raw MCP protocol boilerplate
42
+ - **Validate before running** — `mcpforge validate` runs syntax, security, lint, import, and pytest checks against generated servers
43
+ - **Iterate safely** — `mcpforge update` modifies an existing generated server and backs up changed files before writing
44
+ - **Discover generated servers** — `mcpforge list` finds mcpforge-generated projects in a workspace
45
+ - **Inspect and diagnose** — `mcpforge inspect` summarizes generated server shape, while `mcpforge doctor` checks local readiness
46
+ - **Machine-readable output** — status-like commands expose `--json` for agent workflows
47
+ - **OpenAPI curation controls** — include/exclude tags, operation allowlists, and operation limits keep generated integrations focused
48
+ - **Scaffold without an LLM** — `mcpforge init` creates a minimal FastMCP server skeleton for local iteration
49
+ - **MCP server mode** — `mcpforge-server` exposes generation, planning, validation, inspection, doctor, and discovery tools so AI assistants can build safely
50
+
51
+ ## Quick Start
52
+
53
+ ### Prerequisites
54
+ - Python 3.12+
55
+ - `uv` (recommended)
56
+ - Anthropic API key
57
+
58
+ ### Installation
59
+ ```bash
60
+ uv tool install fastmcp-builder
61
+ ```
62
+
63
+ The PyPI distribution is `fastmcp-builder`; the installed commands remain
64
+ `mcpforge` and `mcpforge-server`.
65
+
66
+ ### Usage
67
+ ```bash
68
+ # Generate a new MCP server
69
+ mcpforge generate "A todo list manager with create, read, update, and delete operations"
70
+
71
+ # Validate an existing generated server
72
+ mcpforge validate ./my-server
73
+
74
+ # Modify an existing generated server
75
+ mcpforge update ./my-server "Add a tool to export todos as CSV"
76
+
77
+ # Find generated servers in the current workspace
78
+ mcpforge list . --recursive
79
+
80
+ # Inspect a generated server without executing it
81
+ mcpforge inspect ./my-server
82
+
83
+ # Check local prerequisites and provider readiness
84
+ mcpforge doctor
85
+ ```
86
+
87
+ Useful generation flags:
88
+ - `--dry-run` displays the structured plan without writing files.
89
+ - `--no-execute` writes files but skips import and test execution.
90
+ - `--strict` treats lint errors as hard validation failures.
91
+ - `--from-openapi FILE` generates from an OpenAPI 3.x spec.
92
+ - `--openapi-include-tag TAG`, `--openapi-exclude-tag TAG`, `--openapi-operation ID`, and `--openapi-limit N` curate OpenAPI conversion.
93
+ - `--language python|typescript` chooses the target server language.
94
+ - `--provider anthropic|openai` selects the generation provider. OpenAI is listed as planned and remains gated until deterministic structured-output smokes are implemented.
95
+
96
+ Useful status flags:
97
+ - `mcpforge list --json`
98
+ - `mcpforge inspect PATH --json`
99
+ - `mcpforge validate PATH --json`
100
+ - `mcpforge doctor --json`
101
+ - `mcpforge version --json`
102
+
103
+ ## Tech Stack
104
+
105
+ | Layer | Technology |
106
+ |-------|------------|
107
+ | Language | Python 3.12+ |
108
+ | Generation | Anthropic Claude via `anthropic` SDK |
109
+ | MCP framework | FastMCP 3.x |
110
+ | CLI | Click 8 |
111
+ | Templates | Jinja2 |
112
+ | Validation | Pydantic v2 |
113
+ | Output | Rich |
114
+
115
+ ## Architecture
116
+
117
+ The `generate` command sends the user's description to Claude with a structured prompt that includes FastMCP 3.x idioms and a tool-schema contract. Claude returns a JSON plan (tool names, signatures, and descriptions) that mcpforge validates against a Pydantic model before rendering through Jinja2 templates into a complete project directory. The generated project is then validated with syntax checks, security scanning, ruff linting, import checks, and pytest execution. The `update` command reads an existing generated server, asks Claude for a targeted modification, writes backups for changed files, and validates the result.
118
+
119
+ ## Current Status
120
+
121
+ As of May 10, 2026, `main` is prepared as `0.3.0` for the `fastmcp-builder` PyPI distribution. The v0.3 builder lane expands mcpforge from runnable-server generation into a production integration builder with inspection, doctor checks, richer generated scaffolds, OpenAPI curation, MCP server parity, provider abstraction, and live generated fixture examples for REST API, filesystem, database, and TypeScript profiles. See `docs/CURRENT-STATE.md` and `docs/ROADMAP-v0.3.md`.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,50 @@
1
+ mcpforge/__init__.py,sha256=wT-IwBzwKTWou8-EZp9HilBLiKvVihTkl_v_j5EhDY8,449
2
+ mcpforge/api_client.py,sha256=-7fpLjexuchgYQPJXNv5xND_qQ1jAFRkUJQBZ2THuCY,5860
3
+ mcpforge/cli.py,sha256=l9mKY1t35QYOpZQrJqXvBrstbw1G9fW0fldL_g8yBAo,32868
4
+ mcpforge/discovery.py,sha256=zP21zm4RSXxx0At1_QYVQ2y-ebTUID7GybqWrqdGWc8,2386
5
+ mcpforge/doctor.py,sha256=FLNcbsKwhkHHxr4UJf-wQWMUIVeeIhl3crizQHX5-u0,3379
6
+ mcpforge/generator.py,sha256=c4AFaw4DpMJpNofH6wPl_4zuXZpOGEUw8aRTg08myKw,2141
7
+ mcpforge/generator_ts.py,sha256=IXYyru0K_r6lobbQB2X6t-4zqXG0cd1w4mP--OqeZr4,1206
8
+ mcpforge/inspection.py,sha256=YJZZnkqK8dTG0WFKfkemmbKQS31ZKippVK_Nbo7FFCg,4867
9
+ mcpforge/mcp_server.py,sha256=FQclaxKmSihqCCb5A-OwvCdOVX8XvoMcuMI4gT_5owQ,9159
10
+ mcpforge/models.py,sha256=i6X49xwYs7Z8oTAes3mUcGBROVPbdWkgIAQXESkz73k,5650
11
+ mcpforge/openapi.py,sha256=l0PMWScd0FLdWSx6_-fAME-MSizxgwIHNxgr0Ue6YQo,8781
12
+ mcpforge/planner.py,sha256=nPGYIIf3XuSmso2lKUNvA336Lt2txfC4K9_oMKTBnac,1995
13
+ mcpforge/prompts/__init__.py,sha256=ECWc4caHwgAondA320i2LBY9xjH-nxe1ClCwMZK0t3Y,716
14
+ mcpforge/prompts/generator.md,sha256=Oy8Kl12HGZn0dX7cMaxb2dEkcB0JNKm76xRJ4Jp0jn0,6658
15
+ mcpforge/prompts/generator_multi.md,sha256=TLg0eDWUrvi5749oDmMHzvFa7ncBdmcYU7_aJDwFJIY,2170
16
+ mcpforge/prompts/generator_ts.md,sha256=h80ppzWNNHfnlkTJ58bhZUMYQfkqSgqN5GZkr2c0Pzk,2070
17
+ mcpforge/prompts/planner.md,sha256=01qkPCHN1Ki1lzaIoM3yn9ybwk6N9W9DFrxbg5-jK_E,6020
18
+ mcpforge/prompts/self_heal.md,sha256=91eV8RAinqoXvx7hGhd_QmgYXUn7Ok6nVLCTgEToTUg,943
19
+ mcpforge/prompts/test_gen.md,sha256=116Ww6DrNXFNy4i1KPH-DojjHYXKWufmC4kg2_yk1F8,2941
20
+ mcpforge/prompts/test_gen_ts.md,sha256=vY5OVasQNj8g_XgdHFycEC3aBVL4HJizwpxFr5n1sE0,1886
21
+ mcpforge/prompts/updater.md,sha256=8WSq6H-mcdqdflKFoIyHdYSVZoACNfz6lYNupaU7yb0,753
22
+ mcpforge/providers.py,sha256=xYoiC7jiIAKLYmDixcpDtnoVqf6gj7jZJbmS9NNOK94,3178
23
+ mcpforge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ mcpforge/sandbox.py,sha256=0x38jOtfATwxSdgQfLyIfpmWdoj1BWTssXPZgXbrxZs,2218
25
+ mcpforge/security.py,sha256=iDhyfdKOG8krOqERwYfbD2u_HqZhMg_Z5nWI0Uq-lGQ,4496
26
+ mcpforge/self_heal.py,sha256=C9ceTOBHF9nMGH_dYD_fhWrN2ioy_BP6OnqIkKNZdLk,7026
27
+ mcpforge/template_hints.py,sha256=0KaKL54d1b1ES4lN8kUIU42sjAq2xHOod0Bfoo1C1CI,3028
28
+ mcpforge/templates/README.md.j2,sha256=MTSNaGEK4wo2SyImTHaqoSXFtMLaWJVw-Ip8B5STYGs,2374
29
+ mcpforge/templates/config.json.j2,sha256=Ex-xDWMyuzmuRGu07r6zyX-SY5SYl0apIpHsE456yGs,287
30
+ mcpforge/templates/env.example.j2,sha256=lXUCyQwvi7Xltg27xgp1sAshMqTUEzB4stKwrKpjSSw,57
31
+ mcpforge/templates/fastmcp.json.j2,sha256=tPLJTuj4lil3vNpTBaUyJ-7tk1lQun5UnptCu8pqn9k,742
32
+ mcpforge/templates/init_server.py.j2,sha256=tlb10xY_EEu-tUzC9WWYMOWZr3H33x4W7TthKO_YyEI,316
33
+ mcpforge/templates/init_test.py.j2,sha256=YtJLC6SSUWNfPpCivL3jdyUON-sRCl87U3-BORRfLLk,266
34
+ mcpforge/templates/pyproject.toml.j2,sha256=KeD3OIIzEcczoMPEgcdPx9UrGEGbXsMvY2Bg3sfD4Zg,381
35
+ mcpforge/templates/ts/README.md.j2,sha256=4J4TKB_4h59_YgTkNWMLRvjBNzj1_04YDRLZ6fXBKvA,548
36
+ mcpforge/templates/ts/config.json.j2,sha256=7QIFtMEPQtSRlVATETEqaemydxVjCPZr64wqHWyaN0c,129
37
+ mcpforge/templates/ts/env.example.j2,sha256=lXUCyQwvi7Xltg27xgp1sAshMqTUEzB4stKwrKpjSSw,57
38
+ mcpforge/templates/ts/gitignore.j2,sha256=TWmWbLSs_Gv5On9EEDDrJRLuZuneiM_J7354ZLWGiPY,34
39
+ mcpforge/templates/ts/package.json.j2,sha256=oTYakFCjI_OOoYLoEvAvMw7YQ5GffbYbwUwPBRKhYO8,503
40
+ mcpforge/templates/ts/tsconfig.json.j2,sha256=bPNP99SGoUiKCUr7YWitD3istXcXdvUOLAAZYAE-3ls,326
41
+ mcpforge/test_generator.py,sha256=o-ZKTw77W3MpCfq--mnq_oQfo0uXftHFHCJjJc1eBNk,869
42
+ mcpforge/updater.py,sha256=Y5njvl5i-2ymYsA6E9HLCKWyXiqP27iubwfLEX2XREE,1853
43
+ mcpforge/utils.py,sha256=DjMxtGMbzEfkKpYzU6eMuSu6F918wSC6lFsujAeIBBo,290
44
+ mcpforge/validator.py,sha256=QawzWqLD4spXIpzfPItoOwwP9Pn8igj5PGFSzrYDFnE,11111
45
+ mcpforge/validator_ts.py,sha256=KWxI-laizuL2fGxlxaHwTYGhop8SRuWF1gJSVGAw69M,4524
46
+ mcpforge/writer.py,sha256=7zuDibuP6cfDp6YGLl6UmeO7T3KOz9S63mMSpt0WA3E,7286
47
+ fastmcp_builder-0.3.0.dist-info/WHEEL,sha256=-unyBNsdNo6Y_XVO25zjvxasG_uUFN_fBd_kLyKE-Fk,81
48
+ fastmcp_builder-0.3.0.dist-info/entry_points.txt,sha256=BtXe9KuwIOsuC9QT_mwUsc-PIxQzCo1MNT-_4Inqm_w,93
49
+ fastmcp_builder-0.3.0.dist-info/METADATA,sha256=Y8rLZ3w24pe4qkbCeqBAKzODJEUmHjsvoZ5EJCTj25s,6109
50
+ fastmcp_builder-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.12
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ mcpforge = mcpforge.cli:cli
3
+ mcpforge-server = mcpforge.mcp_server:mcp.run
4
+
mcpforge/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """mcpforge — Generate FastMCP 3.x MCP servers from plain-English descriptions."""
2
+
3
+ from mcpforge.api_client import DEFAULT_MODEL
4
+ from mcpforge.models import (
5
+ PromptDef,
6
+ ResourceDef,
7
+ ServerPlan,
8
+ ToolDef,
9
+ ToolParam,
10
+ ValidationResult,
11
+ )
12
+
13
+ __version__ = "0.3.0"
14
+ __all__ = [
15
+ "PromptDef",
16
+ "ResourceDef",
17
+ "ServerPlan",
18
+ "ToolDef",
19
+ "ToolParam",
20
+ "ValidationResult",
21
+ "DEFAULT_MODEL",
22
+ "__version__",
23
+ ]
mcpforge/api_client.py ADDED
@@ -0,0 +1,158 @@
1
+ """Anthropic API client wrapper with retry logic.
2
+
3
+ MODEL PIN: This module uses Sonnet 4.6 intentionally. Opus 4.7 rejects
4
+ non-default `temperature` values (400 error), and mcpforge relies on
5
+ `temperature=0` for deterministic JSON output in `generate_json()`.
6
+ Do NOT upgrade the default model to Opus 4.7 (or any future model that
7
+ rejects temperature) without first migrating `generate_json()` to tool
8
+ use with a strict schema, which is the 4.7-era replacement for the
9
+ `temperature=0` determinism lever.
10
+ """
11
+
12
+ import asyncio
13
+ import json
14
+ import os
15
+ import random
16
+ import re
17
+ from collections.abc import AsyncIterator
18
+
19
+ import anthropic
20
+ from pydantic import BaseModel, ValidationError
21
+
22
+ DEFAULT_MODEL = "claude-sonnet-4-6"
23
+
24
+
25
+ class AnthropicClient:
26
+ """Async wrapper around anthropic.AsyncAnthropic with exponential-backoff retry."""
27
+
28
+ def __init__(
29
+ self,
30
+ api_key: str | None = None,
31
+ model: str = DEFAULT_MODEL,
32
+ ) -> None:
33
+ resolved_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
34
+ if not resolved_key:
35
+ raise ValueError(
36
+ "Anthropic API key is required. "
37
+ "Set the ANTHROPIC_API_KEY environment variable or pass api_key= explicitly."
38
+ )
39
+ self._model = model
40
+ self._client = anthropic.AsyncAnthropic(api_key=resolved_key)
41
+
42
+ def __repr__(self) -> str:
43
+ return f"AnthropicClient(model={self._model!r})"
44
+
45
+ async def generate(
46
+ self,
47
+ system_prompt: str,
48
+ user_message: str,
49
+ max_tokens: int = 8192,
50
+ temperature: float = 0.0,
51
+ ) -> str:
52
+ """Send a message and return the text response.
53
+
54
+ Retries up to 3 times with exponential backoff on transient provider errors.
55
+ """
56
+ last_exc: Exception | None = None
57
+ for attempt in range(3):
58
+ try:
59
+ response = await self._client.messages.create(
60
+ model=self._model,
61
+ max_tokens=max_tokens,
62
+ temperature=temperature,
63
+ system=system_prompt,
64
+ messages=[{"role": "user", "content": user_message}],
65
+ )
66
+ return response.content[0].text # type: ignore[union-attr]
67
+ except anthropic.RateLimitError as exc:
68
+ last_exc = exc
69
+ if attempt == 2:
70
+ raise
71
+ wait = (2**attempt) + random.uniform(0.0, 1.0)
72
+ await asyncio.sleep(wait)
73
+ except anthropic.APIConnectionError as exc:
74
+ last_exc = exc
75
+ if attempt == 2:
76
+ raise
77
+ wait = (2**attempt) + random.uniform(0.0, 1.0)
78
+ await asyncio.sleep(wait)
79
+ except anthropic.APIStatusError as exc:
80
+ last_exc = exc
81
+ if exc.status_code >= 500 and attempt < 2:
82
+ wait = (2**attempt) + random.uniform(0.0, 1.0)
83
+ await asyncio.sleep(wait)
84
+ continue
85
+ raise
86
+ raise RuntimeError("generate() exited retry loop unexpectedly") from last_exc
87
+
88
+ async def generate_json(
89
+ self,
90
+ system_prompt: str,
91
+ user_message: str,
92
+ response_model: type[BaseModel],
93
+ max_tokens: int = 8192,
94
+ ) -> BaseModel:
95
+ """Generate and parse a structured JSON response into a Pydantic model instance.
96
+
97
+ Uses temperature=0 for deterministic JSON output. Strips markdown fences
98
+ before parsing so the LLM can optionally wrap its response in ```json blocks.
99
+ """
100
+ text = await self.generate(
101
+ system_prompt=system_prompt,
102
+ user_message=user_message,
103
+ max_tokens=max_tokens,
104
+ temperature=0.0,
105
+ )
106
+ try:
107
+ data = _extract_json(text)
108
+ return response_model.model_validate(data)
109
+ except json.JSONDecodeError as exc:
110
+ raise ValueError(
111
+ f"Response was not valid JSON for {response_model.__name__}.\n"
112
+ f"Raw response (first 500 chars): {text[:500]}"
113
+ ) from exc
114
+ except ValidationError as exc:
115
+ raise ValueError(
116
+ f"Response JSON did not match {response_model.__name__} schema: {exc}"
117
+ ) from exc
118
+
119
+ async def generate_stream(
120
+ self,
121
+ system_prompt: str,
122
+ user_message: str,
123
+ max_tokens: int = 16384,
124
+ temperature: float = 0.2,
125
+ chunk_timeout: float = 60.0,
126
+ ) -> AsyncIterator[str]:
127
+ """Stream text chunks as they arrive from the API.
128
+
129
+ Yields str chunks. No retry — streaming connections are not retryable.
130
+ Raises TimeoutError if no chunk arrives within chunk_timeout seconds.
131
+ """
132
+ async with self._client.messages.stream(
133
+ model=self._model,
134
+ max_tokens=max_tokens,
135
+ temperature=temperature,
136
+ system=system_prompt,
137
+ messages=[{"role": "user", "content": user_message}],
138
+ ) as stream:
139
+ aiter = stream.text_stream.__aiter__()
140
+ while True:
141
+ try:
142
+ text = await asyncio.wait_for(aiter.__anext__(), timeout=chunk_timeout)
143
+ yield text
144
+ except StopAsyncIteration:
145
+ break
146
+ except TimeoutError:
147
+ raise TimeoutError(
148
+ f"Streaming generation stalled — no data received for {chunk_timeout}s"
149
+ ) from None
150
+
151
+
152
+ def _extract_json(text: str) -> dict:
153
+ """Extract a JSON object from text, stripping optional markdown code fences."""
154
+ text = text.strip()
155
+ if text.startswith("```"):
156
+ text = re.sub(r"^```(?:json)?\s*\n?", "", text)
157
+ text = re.sub(r"\n?```\s*$", "", text)
158
+ return json.loads(text.strip())