milton-mcp 0.1.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,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v6
18
+ with:
19
+ python-version: ${{ matrix.python }}
20
+ - run: uv sync --group dev
21
+ - run: uv run ruff check .
22
+ - run: uv run ruff format --check .
23
+ - run: uv run pytest -q
@@ -0,0 +1,39 @@
1
+ name: Release
2
+
3
+ # Push a tag vX.Y.Z matching pyproject's version to publish to PyPI. Publishing uses PyPI
4
+ # trusted publishing: the milton-mcp project on PyPI must list this repo, this workflow file
5
+ # and the `pypi` environment as a trusted publisher.
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v6
16
+ - name: Tag matches the package version
17
+ run: |
18
+ version=$(uv run --no-project python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
19
+ test "v${version}" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != v${version}"; exit 1; }
20
+ - run: uv sync --group dev
21
+ - run: uv run pytest -q
22
+ - run: uv build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment: pypi
32
+ permissions:
33
+ id-token: write
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ name: Schema drift
2
+
3
+ # Checks the bundled schemas.json against Milton's published OpenAPI spec and pricing.
4
+ # A failure means Milton's API changed: run scripts/generate_schemas.py and release.
5
+ on:
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ check:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v6
14
+ - run: uv sync --group dev
15
+ - run: uv run python scripts/generate_schemas.py --check
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Milton Analytics LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.5
2
+ Name: milton-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for Milton's analyst agents: technical, fundamental, macro, trade design and the full desk, through Milton's API.
5
+ Project-URL: Homepage, https://withmilton.ai
6
+ Project-URL: Documentation, https://withmilton.ai/docs/api/v1
7
+ Project-URL: Source, https://github.com/Milton-Analytics-LLC/milton-mcp
8
+ Author-email: Milton Analytics LLC <support@withmilton.ai>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,finance,mcp,milton,model-context-protocol,options,stocks
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Office/Business :: Financial :: Investment
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: mcp<3,>=2.3
20
+ Description-Content-Type: text/markdown
21
+
22
+ # milton-mcp
23
+
24
+ An [MCP](https://modelcontextprotocol.io) server for [Milton](https://withmilton.ai)'s analyst agents. Add it
25
+ to Claude Code, Claude Desktop, Cursor or your own agent, and the agent can ask Milton for a technical,
26
+ fundamental or macro read, a risk-checked trade idea, or the full desk on a ticker.
27
+
28
+ The server is a thin layer over Milton's [public API](https://withmilton.ai/docs/api/v1). The agents run
29
+ on Milton's servers; this package only sends your requests and formats the results. You need a Milton
30
+ account and an API token.
31
+
32
+ ## Install
33
+
34
+ With [uv](https://docs.astral.sh/uv/) (no separate install step; `uvx` fetches and runs it):
35
+
36
+ ```sh
37
+ uvx milton-mcp --version
38
+ ```
39
+
40
+ Or with pip: `pip install milton-mcp`.
41
+
42
+ ## Get an API token
43
+
44
+ Sign in at [withmilton.ai](https://withmilton.ai), open **Settings → API tokens** and create one. It starts
45
+ with `mlt_`. Treat it like a password: it can start runs that bill to your balance.
46
+
47
+ ## Configure your client
48
+
49
+ ### Claude Code
50
+
51
+ ```sh
52
+ claude mcp add milton -e MILTON_API_KEY=mlt_your_token -- uvx milton-mcp
53
+ ```
54
+
55
+ ### Claude Desktop, Cursor and other JSON-configured clients
56
+
57
+ Claude Desktop: `claude_desktop_config.json`. Cursor: `~/.cursor/mcp.json` or `.cursor/mcp.json`.
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "milton": {
63
+ "command": "uvx",
64
+ "args": ["milton-mcp"],
65
+ "env": { "MILTON_API_KEY": "mlt_your_token" }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Your own agent
72
+
73
+ Any MCP client works. With the OpenAI Agents SDK, for example:
74
+
75
+ ```python
76
+ from agents import Agent, Runner
77
+ from agents.mcp import MCPServerStdio
78
+
79
+ async with MCPServerStdio(
80
+ params={"command": "uvx", "args": ["milton-mcp"], "env": {"MILTON_API_KEY": "mlt_your_token"}},
81
+ client_session_timeout_seconds=70,
82
+ ) as milton:
83
+ agent = Agent(name="Analyst", instructions="...", mcp_servers=[milton])
84
+ result = await Runner.run(agent, "Give me a swing-trade technical read on NVDA using gpt-6.1-sol.")
85
+ ```
86
+
87
+ A tool call can wait up to 50 seconds for a run to finish, so give your client a request timeout of at
88
+ least 70 seconds.
89
+
90
+ ## Settings
91
+
92
+ | Variable | Meaning |
93
+ |---|---|
94
+ | `MILTON_API_KEY` | Your API token (`mlt_…`). Required in stdio mode. |
95
+ | `MILTON_DEFAULT_MODEL` | Optional. The model runs use when a call doesn't name one; `model` then becomes optional in the tool schemas. |
96
+ | `MILTON_DEFAULT_EFFORT` | Optional. The reasoning effort runs use when a call doesn't name one. |
97
+ | `MILTON_BASE_URL` | Optional. Defaults to `https://withmilton.ai`. |
98
+
99
+ ## Tools
100
+
101
+ | Tool | What it does |
102
+ |---|---|
103
+ | `milton_technical_analysis` | Multi-timeframe technical read of one US stock or ETF: directional bias and conviction, support and resistance, volatility posture. Inputs: `ticker`, `horizon`. |
104
+ | `milton_fundamental_analysis` | Fundamental read from filings, financials and news: bias, valuation, business quality, drivers, risks. Input: `ticker`. |
105
+ | `milton_macro_analysis` | Market-wide macro read: risk-on or risk-off regime, bias, timing, scheduled event risks, breadth. No ticker. |
106
+ | `milton_quant_analysis` | One risk-checked trade (options or shares) for a ticker and horizon, sized against a hypothetical $100,000 portfolio. It can conclude no trade is worth taking. |
107
+ | `milton_desk` | The full desk: macro, fundamental and technical reads, then a trade designed on them and reviewed by Milton's risk analyst. Takes several minutes. |
108
+ | `milton_get_run` | Gets a run by `run_id`: its status and, once it completes, its result. Uses no tokens. |
109
+
110
+ Every agent tool also takes `model` (any model Milton offers) and an optional `effort` (defaults to
111
+ `high`, or the model's highest level). `horizon` is `intraday`, `swing`, `position` or `long_term`.
112
+
113
+ ### What comes back
114
+
115
+ A short digest the model can read (stance, conviction, summary, key levels, risks, how fresh the data
116
+ is, cost), the full result as structured content, and links to the run in Milton and in the API.
117
+
118
+ A run that hasn't finished after 50 seconds comes back as a `run_id` instead; `milton_get_run` waits up to
119
+ 50 seconds per call for it. A desk run always returns its `run_id` at once.
120
+
121
+ ### Repeated calls
122
+
123
+ MCP clients sometimes re-send a call when a connection drops. An identical call (same tool and inputs)
124
+ within 10 minutes returns the same run and isn't billed again. A call with any different input starts a
125
+ new run.
126
+
127
+ ## Cost
128
+
129
+ Runs bill the tokens they use at the chosen model's rates ([pricing](https://withmilton.ai/api/v1/pricing))
130
+ against your Milton balance; a desk run bills every analyst it runs. Failed runs aren't billed. Pick a
131
+ smaller model or a lower effort for cheaper, faster runs.
132
+
133
+ ## Running it on your own server
134
+
135
+ `--http` serves MCP over Streamable HTTP (stateless, JSON responses), so agents on other machines can
136
+ reach it:
137
+
138
+ ```sh
139
+ milton-mcp --http --host 0.0.0.0 --port 8000 # endpoint: http://<host>:8000/mcp
140
+ ```
141
+
142
+ In HTTP mode the server holds no API token. Every request must carry the caller's token as
143
+ `Authorization: Bearer mlt_…`, which is passed on to Milton; a request without one gets a 401. Put it
144
+ behind TLS if it's reachable beyond your own network.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ uv sync --group dev
150
+ uv run pytest
151
+ uv run ruff check . && uv run ruff format --check .
152
+ ```
153
+
154
+ The tools' input schemas, the result schema and the fallback model list are generated from Milton's
155
+ published API description:
156
+
157
+ ```sh
158
+ uv run python scripts/generate_schemas.py # from https://withmilton.ai
159
+ uv run python scripts/generate_schemas.py --check # fails if src/milton_mcp/schemas.json is stale
160
+ ```
161
+
162
+ Releases publish to PyPI when a `vX.Y.Z` tag matching `pyproject.toml`'s version is pushed.
163
+
164
+ ## Disclosures
165
+
166
+ Milton is not a registered investment adviser, broker-dealer or financial planner. Its outputs are
167
+ AI-generated general information, not personalized investment advice or a recommendation to buy or sell
168
+ any security. Each result carries Milton's full disclosures.
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,151 @@
1
+ # milton-mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for [Milton](https://withmilton.ai)'s analyst agents. Add it
4
+ to Claude Code, Claude Desktop, Cursor or your own agent, and the agent can ask Milton for a technical,
5
+ fundamental or macro read, a risk-checked trade idea, or the full desk on a ticker.
6
+
7
+ The server is a thin layer over Milton's [public API](https://withmilton.ai/docs/api/v1). The agents run
8
+ on Milton's servers; this package only sends your requests and formats the results. You need a Milton
9
+ account and an API token.
10
+
11
+ ## Install
12
+
13
+ With [uv](https://docs.astral.sh/uv/) (no separate install step; `uvx` fetches and runs it):
14
+
15
+ ```sh
16
+ uvx milton-mcp --version
17
+ ```
18
+
19
+ Or with pip: `pip install milton-mcp`.
20
+
21
+ ## Get an API token
22
+
23
+ Sign in at [withmilton.ai](https://withmilton.ai), open **Settings → API tokens** and create one. It starts
24
+ with `mlt_`. Treat it like a password: it can start runs that bill to your balance.
25
+
26
+ ## Configure your client
27
+
28
+ ### Claude Code
29
+
30
+ ```sh
31
+ claude mcp add milton -e MILTON_API_KEY=mlt_your_token -- uvx milton-mcp
32
+ ```
33
+
34
+ ### Claude Desktop, Cursor and other JSON-configured clients
35
+
36
+ Claude Desktop: `claude_desktop_config.json`. Cursor: `~/.cursor/mcp.json` or `.cursor/mcp.json`.
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "milton": {
42
+ "command": "uvx",
43
+ "args": ["milton-mcp"],
44
+ "env": { "MILTON_API_KEY": "mlt_your_token" }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ ### Your own agent
51
+
52
+ Any MCP client works. With the OpenAI Agents SDK, for example:
53
+
54
+ ```python
55
+ from agents import Agent, Runner
56
+ from agents.mcp import MCPServerStdio
57
+
58
+ async with MCPServerStdio(
59
+ params={"command": "uvx", "args": ["milton-mcp"], "env": {"MILTON_API_KEY": "mlt_your_token"}},
60
+ client_session_timeout_seconds=70,
61
+ ) as milton:
62
+ agent = Agent(name="Analyst", instructions="...", mcp_servers=[milton])
63
+ result = await Runner.run(agent, "Give me a swing-trade technical read on NVDA using gpt-6.1-sol.")
64
+ ```
65
+
66
+ A tool call can wait up to 50 seconds for a run to finish, so give your client a request timeout of at
67
+ least 70 seconds.
68
+
69
+ ## Settings
70
+
71
+ | Variable | Meaning |
72
+ |---|---|
73
+ | `MILTON_API_KEY` | Your API token (`mlt_…`). Required in stdio mode. |
74
+ | `MILTON_DEFAULT_MODEL` | Optional. The model runs use when a call doesn't name one; `model` then becomes optional in the tool schemas. |
75
+ | `MILTON_DEFAULT_EFFORT` | Optional. The reasoning effort runs use when a call doesn't name one. |
76
+ | `MILTON_BASE_URL` | Optional. Defaults to `https://withmilton.ai`. |
77
+
78
+ ## Tools
79
+
80
+ | Tool | What it does |
81
+ |---|---|
82
+ | `milton_technical_analysis` | Multi-timeframe technical read of one US stock or ETF: directional bias and conviction, support and resistance, volatility posture. Inputs: `ticker`, `horizon`. |
83
+ | `milton_fundamental_analysis` | Fundamental read from filings, financials and news: bias, valuation, business quality, drivers, risks. Input: `ticker`. |
84
+ | `milton_macro_analysis` | Market-wide macro read: risk-on or risk-off regime, bias, timing, scheduled event risks, breadth. No ticker. |
85
+ | `milton_quant_analysis` | One risk-checked trade (options or shares) for a ticker and horizon, sized against a hypothetical $100,000 portfolio. It can conclude no trade is worth taking. |
86
+ | `milton_desk` | The full desk: macro, fundamental and technical reads, then a trade designed on them and reviewed by Milton's risk analyst. Takes several minutes. |
87
+ | `milton_get_run` | Gets a run by `run_id`: its status and, once it completes, its result. Uses no tokens. |
88
+
89
+ Every agent tool also takes `model` (any model Milton offers) and an optional `effort` (defaults to
90
+ `high`, or the model's highest level). `horizon` is `intraday`, `swing`, `position` or `long_term`.
91
+
92
+ ### What comes back
93
+
94
+ A short digest the model can read (stance, conviction, summary, key levels, risks, how fresh the data
95
+ is, cost), the full result as structured content, and links to the run in Milton and in the API.
96
+
97
+ A run that hasn't finished after 50 seconds comes back as a `run_id` instead; `milton_get_run` waits up to
98
+ 50 seconds per call for it. A desk run always returns its `run_id` at once.
99
+
100
+ ### Repeated calls
101
+
102
+ MCP clients sometimes re-send a call when a connection drops. An identical call (same tool and inputs)
103
+ within 10 minutes returns the same run and isn't billed again. A call with any different input starts a
104
+ new run.
105
+
106
+ ## Cost
107
+
108
+ Runs bill the tokens they use at the chosen model's rates ([pricing](https://withmilton.ai/api/v1/pricing))
109
+ against your Milton balance; a desk run bills every analyst it runs. Failed runs aren't billed. Pick a
110
+ smaller model or a lower effort for cheaper, faster runs.
111
+
112
+ ## Running it on your own server
113
+
114
+ `--http` serves MCP over Streamable HTTP (stateless, JSON responses), so agents on other machines can
115
+ reach it:
116
+
117
+ ```sh
118
+ milton-mcp --http --host 0.0.0.0 --port 8000 # endpoint: http://<host>:8000/mcp
119
+ ```
120
+
121
+ In HTTP mode the server holds no API token. Every request must carry the caller's token as
122
+ `Authorization: Bearer mlt_…`, which is passed on to Milton; a request without one gets a 401. Put it
123
+ behind TLS if it's reachable beyond your own network.
124
+
125
+ ## Development
126
+
127
+ ```sh
128
+ uv sync --group dev
129
+ uv run pytest
130
+ uv run ruff check . && uv run ruff format --check .
131
+ ```
132
+
133
+ The tools' input schemas, the result schema and the fallback model list are generated from Milton's
134
+ published API description:
135
+
136
+ ```sh
137
+ uv run python scripts/generate_schemas.py # from https://withmilton.ai
138
+ uv run python scripts/generate_schemas.py --check # fails if src/milton_mcp/schemas.json is stale
139
+ ```
140
+
141
+ Releases publish to PyPI when a `vX.Y.Z` tag matching `pyproject.toml`'s version is pushed.
142
+
143
+ ## Disclosures
144
+
145
+ Milton is not a registered investment adviser, broker-dealer or financial planner. Its outputs are
146
+ AI-generated general information, not personalized investment advice or a recommendation to buy or sell
147
+ any security. Each result carries Milton's full disclosures.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "milton-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for Milton's analyst agents: technical, fundamental, macro, trade design and the full desk, through Milton's API."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Milton Analytics LLC", email = "support@withmilton.ai" }]
14
+ keywords = ["mcp", "model-context-protocol", "milton", "finance", "agents", "stocks", "options"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Financial and Insurance Industry",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Office/Business :: Financial :: Investment",
22
+ ]
23
+ dependencies = ["mcp>=2.3,<3"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://withmilton.ai"
27
+ Documentation = "https://withmilton.ai/docs/api/v1"
28
+ Source = "https://github.com/Milton-Analytics-LLC/milton-mcp"
29
+
30
+ [project.scripts]
31
+ milton-mcp = "milton_mcp.__main__:main"
32
+
33
+ [dependency-groups]
34
+ dev = ["pytest>=8", "ruff==0.15.15", "pyyaml>=6"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/milton_mcp"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ pythonpath = ["src", "."]
42
+
43
+ [tool.ruff]
44
+ line-length = 120
45
+ target-version = "py310"
46
+
47
+ [tool.ruff.lint]
48
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
@@ -0,0 +1,130 @@
1
+ """Generate src/milton_mcp/schemas.json from Milton's published OpenAPI spec and pricing.
2
+
3
+ The tools' input schemas, the run envelope (each tool's outputSchema) and the fallback model list
4
+ all come from Milton's public API description, so the server never drifts from the API it calls:
5
+
6
+ python scripts/generate_schemas.py # from https://withmilton.ai
7
+ python scripts/generate_schemas.py --spec ../milton/go-services/cmd/api/openapi.yaml \\
8
+ --pricing pricing.json # from a local checkout
9
+ python scripts/generate_schemas.py --check # exit 1 if the bundled copy is stale
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import copy
16
+ import json
17
+ import sys
18
+ import urllib.request
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import yaml
23
+
24
+ DEFAULT_BASE = "https://withmilton.ai"
25
+ OUT_PATH = Path(__file__).resolve().parents[1] / "src" / "milton_mcp" / "schemas.json"
26
+
27
+ # The agents this server exposes. risk and portfolio are left out for now; desk runs without
28
+ # portfolio_id (it sizes on a hypothetical portfolio).
29
+ AGENTS = ("technical", "fundamental", "macro", "quant", "desk")
30
+ DROPPED_FIELDS = {"portfolio_id"}
31
+ # Added by the server itself, with the offered models and effort levels as enums.
32
+ MODEL_FIELDS = {"model", "effort"}
33
+ # Top-level combinators some MCP hosts reject in a tool's input schema.
34
+ TOP_LEVEL_COMBINATORS = {"oneOf", "anyOf", "allOf", "dependentRequired"}
35
+ # OpenAPI-only formats; strict JSON Schema validators reject an unknown format.
36
+ OPENAPI_FORMATS = {"int32", "int64", "float", "double", "byte", "binary", "password"}
37
+
38
+
39
+ def _load(source: str) -> str:
40
+ if source.startswith(("http://", "https://")):
41
+ # withmilton.ai's Cloudflare rules answer urllib's default "Python-urllib/x.y" with a 403.
42
+ request = urllib.request.Request(source, headers={"User-Agent": "milton-mcp-schema-generator"})
43
+ with urllib.request.urlopen(request, timeout=30) as resp:
44
+ return resp.read().decode()
45
+ return Path(source).read_text()
46
+
47
+
48
+ def _camel(name: str) -> str:
49
+ return "".join(part.capitalize() for part in name.split("_"))
50
+
51
+
52
+ def _input_schema(spec: dict[str, Any], agent: str) -> dict[str, Any]:
53
+ schema = copy.deepcopy(spec["components"]["schemas"][f"{_camel(agent)}RunRequest"])
54
+ for key in TOP_LEVEL_COMBINATORS | {"description"}:
55
+ schema.pop(key, None)
56
+ props = schema.get("properties", {})
57
+ for name in DROPPED_FIELDS | MODEL_FIELDS:
58
+ props.pop(name, None)
59
+ schema["properties"] = props
60
+ schema["required"] = [r for r in schema.get("required", []) if r in props]
61
+ schema["additionalProperties"] = False
62
+ return schema
63
+
64
+
65
+ def _waits(spec: dict[str, Any], agent: str) -> bool:
66
+ """Whether the start route takes ?wait (desk, the long-running agent, doesn't)."""
67
+ params = spec["paths"][f"/api/v1/agents/{agent}/runs"]["post"].get("parameters", [])
68
+ return any(p.get("$ref", "").endswith("/AgentRunWait") for p in params)
69
+
70
+
71
+ def _to_json_schema(node: Any, schemas: dict[str, Any]) -> Any:
72
+ """OpenAPI 3.0 → JSON Schema 2020-12: inline component $refs, nullable → anyOf with null."""
73
+ if isinstance(node, list):
74
+ return [_to_json_schema(x, schemas) for x in node]
75
+ if not isinstance(node, dict):
76
+ return node
77
+ ref = node.get("$ref")
78
+ if isinstance(ref, str) and ref.startswith("#/components/schemas/"):
79
+ return _to_json_schema(schemas[ref.removeprefix("#/components/schemas/")], schemas)
80
+ out = {k: _to_json_schema(v, schemas) for k, v in node.items() if k != "nullable"}
81
+ if out.get("format") in OPENAPI_FORMATS:
82
+ del out["format"]
83
+ if node.get("nullable") is True:
84
+ # anyOf with single-type branches, not a type array: hosts that map tool schemas onto a
85
+ # single-`type` dialect reject type arrays.
86
+ description = out.pop("description", None)
87
+ out = {"anyOf": [out, {"type": "null"}]}
88
+ if description:
89
+ out["description"] = description
90
+ return out
91
+
92
+
93
+ def build(spec_text: str, pricing: dict[str, Any]) -> dict[str, Any]:
94
+ spec = yaml.safe_load(spec_text)
95
+ schemas = spec["components"]["schemas"]
96
+ agents = {agent: {"input_schema": _input_schema(spec, agent), "waits": _waits(spec, agent)} for agent in AGENTS}
97
+ envelope = _to_json_schema(schemas["AgentRunEnvelope"], schemas)
98
+ efforts = pricing.get("efforts")
99
+ if not efforts:
100
+ raise SystemExit("pricing has no efforts: the API predates per-run model and effort")
101
+ return {
102
+ "agents": agents,
103
+ "envelope_schema": envelope,
104
+ "models": {model: list(levels) for model, levels in sorted(efforts.items())},
105
+ }
106
+
107
+
108
+ def render(data: dict[str, Any]) -> str:
109
+ return json.dumps(data, indent=2, sort_keys=True) + "\n"
110
+
111
+
112
+ def main(argv: list[str] | None = None) -> int:
113
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
114
+ parser.add_argument("--spec", default=f"{DEFAULT_BASE}/api/v1/openapi.yaml", help="OpenAPI spec URL or path")
115
+ parser.add_argument("--pricing", default=f"{DEFAULT_BASE}/api/v1/pricing", help="pricing JSON URL or path")
116
+ parser.add_argument("--check", action="store_true", help="fail if the bundled schemas.json is stale")
117
+ args = parser.parse_args(argv)
118
+
119
+ text = render(build(_load(args.spec), json.loads(_load(args.pricing))))
120
+ if args.check:
121
+ if OUT_PATH.read_text() != text:
122
+ print(f"{OUT_PATH} is stale; run scripts/generate_schemas.py", file=sys.stderr)
123
+ return 1
124
+ return 0
125
+ OUT_PATH.write_text(text)
126
+ return 0
127
+
128
+
129
+ if __name__ == "__main__":
130
+ sys.exit(main())
@@ -0,0 +1,9 @@
1
+ """MCP server for Milton's analyst agents."""
2
+
3
+ import importlib.metadata
4
+
5
+ # pyproject.toml is the one place the version is set; the release workflow checks the tag against it.
6
+ try:
7
+ __version__ = importlib.metadata.version("milton-mcp")
8
+ except importlib.metadata.PackageNotFoundError:
9
+ __version__ = "0+unknown"