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.
- milton_mcp-0.1.0/.github/workflows/ci.yaml +23 -0
- milton_mcp-0.1.0/.github/workflows/release.yaml +39 -0
- milton_mcp-0.1.0/.github/workflows/schema-drift.yaml +15 -0
- milton_mcp-0.1.0/.gitignore +9 -0
- milton_mcp-0.1.0/LICENSE +21 -0
- milton_mcp-0.1.0/PKG-INFO +172 -0
- milton_mcp-0.1.0/README.md +151 -0
- milton_mcp-0.1.0/pyproject.toml +48 -0
- milton_mcp-0.1.0/scripts/generate_schemas.py +130 -0
- milton_mcp-0.1.0/src/milton_mcp/__init__.py +9 -0
- milton_mcp-0.1.0/src/milton_mcp/__main__.py +111 -0
- milton_mcp-0.1.0/src/milton_mcp/client.py +90 -0
- milton_mcp-0.1.0/src/milton_mcp/config.py +36 -0
- milton_mcp-0.1.0/src/milton_mcp/digest.py +146 -0
- milton_mcp-0.1.0/src/milton_mcp/errors.py +84 -0
- milton_mcp-0.1.0/src/milton_mcp/idempotency.py +72 -0
- milton_mcp-0.1.0/src/milton_mcp/schemas.json +445 -0
- milton_mcp-0.1.0/src/milton_mcp/server.py +140 -0
- milton_mcp-0.1.0/src/milton_mcp/tools.py +187 -0
- milton_mcp-0.1.0/tests/__init__.py +0 -0
- milton_mcp-0.1.0/tests/conftest.py +29 -0
- milton_mcp-0.1.0/tests/fakes.py +136 -0
- milton_mcp-0.1.0/tests/test_cli.py +38 -0
- milton_mcp-0.1.0/tests/test_errors.py +93 -0
- milton_mcp-0.1.0/tests/test_generate_schemas.py +131 -0
- milton_mcp-0.1.0/tests/test_http_mode.py +59 -0
- milton_mcp-0.1.0/tests/test_idempotency.py +77 -0
- milton_mcp-0.1.0/tests/test_runs.py +175 -0
- milton_mcp-0.1.0/tests/test_tools_list.py +97 -0
- milton_mcp-0.1.0/tests/test_version.py +50 -0
- milton_mcp-0.1.0/uv.lock +1327 -0
|
@@ -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
|
milton_mcp-0.1.0/LICENSE
ADDED
|
@@ -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"
|