mcpify-openapi 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.
- mcpify_openapi-1.0.0/LICENSE +21 -0
- mcpify_openapi-1.0.0/PKG-INFO +208 -0
- mcpify_openapi-1.0.0/README.md +182 -0
- mcpify_openapi-1.0.0/mcpify/__init__.py +17 -0
- mcpify_openapi-1.0.0/mcpify/api_server.py +139 -0
- mcpify_openapi-1.0.0/mcpify/cli.py +238 -0
- mcpify_openapi-1.0.0/mcpify/http_client.py +52 -0
- mcpify_openapi-1.0.0/mcpify/spec.py +126 -0
- mcpify_openapi-1.0.0/mcpify/tools.py +274 -0
- mcpify_openapi-1.0.0/mcpify_openapi.egg-info/PKG-INFO +208 -0
- mcpify_openapi-1.0.0/mcpify_openapi.egg-info/SOURCES.txt +20 -0
- mcpify_openapi-1.0.0/mcpify_openapi.egg-info/dependency_links.txt +1 -0
- mcpify_openapi-1.0.0/mcpify_openapi.egg-info/entry_points.txt +2 -0
- mcpify_openapi-1.0.0/mcpify_openapi.egg-info/requires.txt +11 -0
- mcpify_openapi-1.0.0/mcpify_openapi.egg-info/top_level.txt +1 -0
- mcpify_openapi-1.0.0/pyproject.toml +55 -0
- mcpify_openapi-1.0.0/setup.cfg +4 -0
- mcpify_openapi-1.0.0/tests/test_cli.py +152 -0
- mcpify_openapi-1.0.0/tests/test_e2e.py +172 -0
- mcpify_openapi-1.0.0/tests/test_policy.py +108 -0
- mcpify_openapi-1.0.0/tests/test_spec.py +136 -0
- mcpify_openapi-1.0.0/tests/test_tools.py +216 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Furkan GΓΆktan
|
|
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,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcpify-openapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Turn any OpenAPI REST API into an MCP server so AI agents can call it β zero dependencies
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: mcp,model-context-protocol,openapi,swagger,ai-agents,claude,rest,api,cli
|
|
7
|
+
Classifier: Environment :: Console
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Provides-Extra: yaml
|
|
17
|
+
Requires-Dist: pyyaml>=6.0.3; extra == "yaml"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.16.4; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=2.3.1; extra == "dev"
|
|
23
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
24
|
+
Requires-Dist: pyyaml>=6.0.3; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# π mcpify
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
π English | [TΓΌrkΓ§e](README.tr.md)
|
|
33
|
+
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="docs/demo.gif" alt="mcpify in action β listing and serving OpenAPI endpoints as MCP tools" width="720">
|
|
36
|
+
</p>
|
|
37
|
+

|
|
38
|
+
[](https://github.com/furkan708/mcpify/actions/workflows/ci.yml)  
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
**Turn any OpenAPI REST API into an [MCP](https://modelcontextprotocol.io) server** β so Claude Code, Cursor, and every other MCP client can call your API directly. One command. Zero dependencies.
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
Your company has a REST API. Your AI agent needs to call it. Until now that
|
|
46
|
+
meant hand-writing a custom MCP server for every API. With mcpify:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
mcpify serve https://your-company.com/openapi.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's it β every endpoint just became a tool your AI agent can discover, understand, and call.
|
|
53
|
+
|
|
54
|
+
π **Deep docs:** [Usage guide](docs/USAGE.md) β auth patterns, scoping, Docker, troubleshooting Β· [Architecture](docs/ARCHITECTURE.md) Β· [Contributing](CONTRIBUTING.md) Β· [Changelog](CHANGELOG.md) Β· [Security](SECURITY.md)
|
|
55
|
+
|
|
56
|
+
## β¨ Why you'll like it
|
|
57
|
+
|
|
58
|
+
- β‘ **60 seconds to working** β point it at any OpenAPI 3.x spec (file or URL)
|
|
59
|
+
- π **Credentials never touch the spec or the model** β pulled from your
|
|
60
|
+
environment at call time (`--auth-env`), sent as `Authorization: Bearer`,
|
|
61
|
+
a custom header, or a query parameter
|
|
62
|
+
- π§° **Every operation becomes a first-class MCP tool** β input schemas are
|
|
63
|
+
generated from `parameters` + `requestBody`, internal `$ref`s are resolved
|
|
64
|
+
- ποΈ **Scope it down** β `--read-only` (GET only), `--tag payments`,
|
|
65
|
+
`--include /v1/orders`, `--exclude /admin`, plus a policy layer for real-world
|
|
66
|
+
APIs: `--deny REGEX` hides mutating GETs, `--allow REGEX` re-includes
|
|
67
|
+
read-style POST endpoints. Deny always wins.
|
|
68
|
+
- π©Ί **`mcpify doctor`** β tells you if your spec is agent-friendly before you ship
|
|
69
|
+
- πͺΆ **Zero dependencies** β one pure-Python file tree; YAML specs need an
|
|
70
|
+
optional `pip install 'mcpify[yaml]'`
|
|
71
|
+
- π§ͺ **53 tests** including a full end-to-end suite against a real local HTTP API
|
|
72
|
+
|
|
73
|
+
## π Quick start
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/furkan708/mcpify.git
|
|
77
|
+
cd mcpify && pip install .
|
|
78
|
+
|
|
79
|
+
# 1. preview the tools that will be generated
|
|
80
|
+
mcpify list examples/petstore.json
|
|
81
|
+
|
|
82
|
+
# 2. validate the spec is agent-friendly
|
|
83
|
+
mcpify doctor examples/petstore.json
|
|
84
|
+
|
|
85
|
+
# 3. serve it over MCP
|
|
86
|
+
mcpify serve examples/petstore.json --base-url https://petstore.example.com/v1
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### With authentication
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Bearer token read from the environment (never hardcoded)
|
|
93
|
+
export PETSTORE_KEY="sk-..."
|
|
94
|
+
mcpify serve petstore.json \
|
|
95
|
+
--base-url https://petstore.example.com/v1 \
|
|
96
|
+
--auth-env PETSTORE_KEY \
|
|
97
|
+
--auth-style bearer \
|
|
98
|
+
--read-only
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Flag | Meaning |
|
|
102
|
+
| ---- | ------- |
|
|
103
|
+
| `--auth-env VAR` | environment variable holding the credential |
|
|
104
|
+
| `--auth-style bearer\|header\|query` | how it is sent |
|
|
105
|
+
| `--auth-name NAME` | header / query name for non-bearer styles (e.g. `X-API-Key`) |
|
|
106
|
+
|
|
107
|
+
## π€ Plug it into your agent
|
|
108
|
+
|
|
109
|
+
**Claude Code:**
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
claude mcp add my-api -- mcpify serve openapi.json --read-only
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Claude Desktop / Cursor / any MCP client** (`claude_desktop_config.json`):
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"petstore": {
|
|
121
|
+
"command": "mcpify",
|
|
122
|
+
"args": ["serve", "~/specs/petstore.json", "--auth-env", "PETSTORE_KEY"]
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Now ask your agent: *"list the pets, then create one named Milo"* β it discovers `list_pets` and `create_pet`, fills the arguments, and performs real HTTP calls.
|
|
129
|
+
|
|
130
|
+
## π How operations become tools
|
|
131
|
+
|
|
132
|
+
| OpenAPI | mcpify |
|
|
133
|
+
| ------- | ------ |
|
|
134
|
+
| `operationId` | tool name (sanitized; falls back to `method_path`) |
|
|
135
|
+
| `summary` / `description` | tool description the agent reads |
|
|
136
|
+
| `parameters` (path/query/header) | individual typed arguments with enums |
|
|
137
|
+
| `requestBody` (JSON) | a `body` object argument |
|
|
138
|
+
| `$ref` pointers | resolved inline (components β real schemas) |
|
|
139
|
+
| `servers[0].url` | default base URL (override: `--base-url`) |
|
|
140
|
+
|
|
141
|
+
The agent only ever sees the tool list and your API's JSON responses β
|
|
142
|
+
mcpify adds no middleware, caches nothing, and sends credentials nowhere
|
|
143
|
+
except your API.
|
|
144
|
+
|
|
145
|
+
## π©Ί Doctor
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
$ mcpify doctor my-api.json
|
|
149
|
+
openapi: 3.0.3
|
|
150
|
+
title: Acme API
|
|
151
|
+
paths: 23
|
|
152
|
+
tools: 41 operations
|
|
153
|
+
servers: https://api.acme.com
|
|
154
|
+
warning: 12/41 operations have no operationId (names fall back to method_path)
|
|
155
|
+
warning: 30/41 operations have no summary (agents see no description)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## π CLI reference
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
mcpify list <spec> [--tag T] [--include P] [--exclude P] [--read-only] [--json]
|
|
162
|
+
mcpify serve <spec> [--base-url URL] [--name N] [--auth-env VAR]
|
|
163
|
+
[--auth-style bearer|header|query] [--auth-name NAME]
|
|
164
|
+
[--timeout S] [--read-only] [--tag T] [--include P] [--exclude P]
|
|
165
|
+
mcpify doctor <spec>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Notes & limitations
|
|
169
|
+
|
|
170
|
+
- JSON specs work out of the box; YAML specs need `pip install 'mcpify[yaml]'`
|
|
171
|
+
- Only local `$ref` pointers are resolved (bundle external docs first β most tools do anyway)
|
|
172
|
+
- Request bodies are exposed as a single `body` object argument β predictable over clever
|
|
173
|
+
- Spec versions: OpenAPI 3.x and Swagger 2.x roots are accepted; 3.x is the happy path
|
|
174
|
+
|
|
175
|
+
## π§ͺ Tests
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pip install pytest pyyaml
|
|
179
|
+
pytest -v
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The e2e suite boots a real local HTTP API and drives the full MCP protocol
|
|
183
|
+
over stdio β initialize β tools/list β tools/call β and asserts on the HTTP
|
|
184
|
+
requests that hit the wire.
|
|
185
|
+
|
|
186
|
+
## ποΈ Project Structure
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
mcpify/
|
|
190
|
+
βββ mcpify/
|
|
191
|
+
β βββ spec.py # OpenAPI loading, $ref resolution, operation walking
|
|
192
|
+
β βββ tools.py # operation -> MCP tool, argument -> HTTP request
|
|
193
|
+
β βββ http_client.py # execution (urllib, HTTP errors become tool results)
|
|
194
|
+
β βββ api_server.py # MCP stdio server (JSON-RPC 2.0)
|
|
195
|
+
β βββ cli.py # list / serve / doctor
|
|
196
|
+
βββ examples/petstore.json
|
|
197
|
+
βββ tests/
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## πΊοΈ Roadmap
|
|
201
|
+
|
|
202
|
+
- [ ] `--output-server FILE` β generate a standalone, shareable server script
|
|
203
|
+
- [ ] Per-operation rate limiting
|
|
204
|
+
- [ ] OAuth2 client-credentials flow
|
|
205
|
+
|
|
206
|
+
## π License
|
|
207
|
+
|
|
208
|
+
MIT β see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# π mcpify
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
π English | [TΓΌrkΓ§e](README.tr.md)
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<img src="docs/demo.gif" alt="mcpify in action β listing and serving OpenAPI endpoints as MCP tools" width="720">
|
|
10
|
+
</p>
|
|
11
|
+

|
|
12
|
+
[](https://github.com/furkan708/mcpify/actions/workflows/ci.yml)  
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
**Turn any OpenAPI REST API into an [MCP](https://modelcontextprotocol.io) server** β so Claude Code, Cursor, and every other MCP client can call your API directly. One command. Zero dependencies.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
Your company has a REST API. Your AI agent needs to call it. Until now that
|
|
20
|
+
meant hand-writing a custom MCP server for every API. With mcpify:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
mcpify serve https://your-company.com/openapi.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it β every endpoint just became a tool your AI agent can discover, understand, and call.
|
|
27
|
+
|
|
28
|
+
π **Deep docs:** [Usage guide](docs/USAGE.md) β auth patterns, scoping, Docker, troubleshooting Β· [Architecture](docs/ARCHITECTURE.md) Β· [Contributing](CONTRIBUTING.md) Β· [Changelog](CHANGELOG.md) Β· [Security](SECURITY.md)
|
|
29
|
+
|
|
30
|
+
## β¨ Why you'll like it
|
|
31
|
+
|
|
32
|
+
- β‘ **60 seconds to working** β point it at any OpenAPI 3.x spec (file or URL)
|
|
33
|
+
- π **Credentials never touch the spec or the model** β pulled from your
|
|
34
|
+
environment at call time (`--auth-env`), sent as `Authorization: Bearer`,
|
|
35
|
+
a custom header, or a query parameter
|
|
36
|
+
- π§° **Every operation becomes a first-class MCP tool** β input schemas are
|
|
37
|
+
generated from `parameters` + `requestBody`, internal `$ref`s are resolved
|
|
38
|
+
- ποΈ **Scope it down** β `--read-only` (GET only), `--tag payments`,
|
|
39
|
+
`--include /v1/orders`, `--exclude /admin`, plus a policy layer for real-world
|
|
40
|
+
APIs: `--deny REGEX` hides mutating GETs, `--allow REGEX` re-includes
|
|
41
|
+
read-style POST endpoints. Deny always wins.
|
|
42
|
+
- π©Ί **`mcpify doctor`** β tells you if your spec is agent-friendly before you ship
|
|
43
|
+
- πͺΆ **Zero dependencies** β one pure-Python file tree; YAML specs need an
|
|
44
|
+
optional `pip install 'mcpify[yaml]'`
|
|
45
|
+
- π§ͺ **53 tests** including a full end-to-end suite against a real local HTTP API
|
|
46
|
+
|
|
47
|
+
## π Quick start
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/furkan708/mcpify.git
|
|
51
|
+
cd mcpify && pip install .
|
|
52
|
+
|
|
53
|
+
# 1. preview the tools that will be generated
|
|
54
|
+
mcpify list examples/petstore.json
|
|
55
|
+
|
|
56
|
+
# 2. validate the spec is agent-friendly
|
|
57
|
+
mcpify doctor examples/petstore.json
|
|
58
|
+
|
|
59
|
+
# 3. serve it over MCP
|
|
60
|
+
mcpify serve examples/petstore.json --base-url https://petstore.example.com/v1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### With authentication
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Bearer token read from the environment (never hardcoded)
|
|
67
|
+
export PETSTORE_KEY="sk-..."
|
|
68
|
+
mcpify serve petstore.json \
|
|
69
|
+
--base-url https://petstore.example.com/v1 \
|
|
70
|
+
--auth-env PETSTORE_KEY \
|
|
71
|
+
--auth-style bearer \
|
|
72
|
+
--read-only
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Flag | Meaning |
|
|
76
|
+
| ---- | ------- |
|
|
77
|
+
| `--auth-env VAR` | environment variable holding the credential |
|
|
78
|
+
| `--auth-style bearer\|header\|query` | how it is sent |
|
|
79
|
+
| `--auth-name NAME` | header / query name for non-bearer styles (e.g. `X-API-Key`) |
|
|
80
|
+
|
|
81
|
+
## π€ Plug it into your agent
|
|
82
|
+
|
|
83
|
+
**Claude Code:**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
claude mcp add my-api -- mcpify serve openapi.json --read-only
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Claude Desktop / Cursor / any MCP client** (`claude_desktop_config.json`):
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"petstore": {
|
|
95
|
+
"command": "mcpify",
|
|
96
|
+
"args": ["serve", "~/specs/petstore.json", "--auth-env", "PETSTORE_KEY"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Now ask your agent: *"list the pets, then create one named Milo"* β it discovers `list_pets` and `create_pet`, fills the arguments, and performs real HTTP calls.
|
|
103
|
+
|
|
104
|
+
## π How operations become tools
|
|
105
|
+
|
|
106
|
+
| OpenAPI | mcpify |
|
|
107
|
+
| ------- | ------ |
|
|
108
|
+
| `operationId` | tool name (sanitized; falls back to `method_path`) |
|
|
109
|
+
| `summary` / `description` | tool description the agent reads |
|
|
110
|
+
| `parameters` (path/query/header) | individual typed arguments with enums |
|
|
111
|
+
| `requestBody` (JSON) | a `body` object argument |
|
|
112
|
+
| `$ref` pointers | resolved inline (components β real schemas) |
|
|
113
|
+
| `servers[0].url` | default base URL (override: `--base-url`) |
|
|
114
|
+
|
|
115
|
+
The agent only ever sees the tool list and your API's JSON responses β
|
|
116
|
+
mcpify adds no middleware, caches nothing, and sends credentials nowhere
|
|
117
|
+
except your API.
|
|
118
|
+
|
|
119
|
+
## π©Ί Doctor
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
$ mcpify doctor my-api.json
|
|
123
|
+
openapi: 3.0.3
|
|
124
|
+
title: Acme API
|
|
125
|
+
paths: 23
|
|
126
|
+
tools: 41 operations
|
|
127
|
+
servers: https://api.acme.com
|
|
128
|
+
warning: 12/41 operations have no operationId (names fall back to method_path)
|
|
129
|
+
warning: 30/41 operations have no summary (agents see no description)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## π CLI reference
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
mcpify list <spec> [--tag T] [--include P] [--exclude P] [--read-only] [--json]
|
|
136
|
+
mcpify serve <spec> [--base-url URL] [--name N] [--auth-env VAR]
|
|
137
|
+
[--auth-style bearer|header|query] [--auth-name NAME]
|
|
138
|
+
[--timeout S] [--read-only] [--tag T] [--include P] [--exclude P]
|
|
139
|
+
mcpify doctor <spec>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Notes & limitations
|
|
143
|
+
|
|
144
|
+
- JSON specs work out of the box; YAML specs need `pip install 'mcpify[yaml]'`
|
|
145
|
+
- Only local `$ref` pointers are resolved (bundle external docs first β most tools do anyway)
|
|
146
|
+
- Request bodies are exposed as a single `body` object argument β predictable over clever
|
|
147
|
+
- Spec versions: OpenAPI 3.x and Swagger 2.x roots are accepted; 3.x is the happy path
|
|
148
|
+
|
|
149
|
+
## π§ͺ Tests
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install pytest pyyaml
|
|
153
|
+
pytest -v
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The e2e suite boots a real local HTTP API and drives the full MCP protocol
|
|
157
|
+
over stdio β initialize β tools/list β tools/call β and asserts on the HTTP
|
|
158
|
+
requests that hit the wire.
|
|
159
|
+
|
|
160
|
+
## ποΈ Project Structure
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
mcpify/
|
|
164
|
+
βββ mcpify/
|
|
165
|
+
β βββ spec.py # OpenAPI loading, $ref resolution, operation walking
|
|
166
|
+
β βββ tools.py # operation -> MCP tool, argument -> HTTP request
|
|
167
|
+
β βββ http_client.py # execution (urllib, HTTP errors become tool results)
|
|
168
|
+
β βββ api_server.py # MCP stdio server (JSON-RPC 2.0)
|
|
169
|
+
β βββ cli.py # list / serve / doctor
|
|
170
|
+
βββ examples/petstore.json
|
|
171
|
+
βββ tests/
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## πΊοΈ Roadmap
|
|
175
|
+
|
|
176
|
+
- [ ] `--output-server FILE` β generate a standalone, shareable server script
|
|
177
|
+
- [ ] Per-operation rate limiting
|
|
178
|
+
- [ ] OAuth2 client-credentials flow
|
|
179
|
+
|
|
180
|
+
## π License
|
|
181
|
+
|
|
182
|
+
MIT β see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""mcpify β turn any OpenAPI REST API into an MCP server for AI agents."""
|
|
2
|
+
|
|
3
|
+
from .spec import SpecError, load_spec, resolve_ref, resolve_schema
|
|
4
|
+
from .tools import AuthConfig, RequestError, build_request, spec_to_tools
|
|
5
|
+
|
|
6
|
+
__version__ = "1.0.0"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SpecError",
|
|
9
|
+
"load_spec",
|
|
10
|
+
"resolve_ref",
|
|
11
|
+
"resolve_schema",
|
|
12
|
+
"AuthConfig",
|
|
13
|
+
"RequestError",
|
|
14
|
+
"build_request",
|
|
15
|
+
"spec_to_tools",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) stdio server that exposes an OpenAPI spec.
|
|
2
|
+
|
|
3
|
+
Specks newline-delimited JSON-RPC 2.0 over stdio, as used by MCP stdio
|
|
4
|
+
transports. Every OpenAPI operation becomes an MCP tool that performs a
|
|
5
|
+
real HTTP call against the configured base URL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .http_client import execute, format_result
|
|
15
|
+
from .tools import AuthConfig, RequestError, build_request, spec_to_tools
|
|
16
|
+
|
|
17
|
+
PROTOCOL_VERSION = "2025-06-18"
|
|
18
|
+
SERVER_NAME = "mcpify"
|
|
19
|
+
SERVER_VERSION = "1.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ApiServer:
|
|
23
|
+
"""MCP handler backed by one OpenAPI specification."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
spec: dict,
|
|
28
|
+
base_url: str,
|
|
29
|
+
server_name: str = "mcpify",
|
|
30
|
+
auth: AuthConfig | None = None,
|
|
31
|
+
timeout: float = 30.0,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.spec = spec
|
|
34
|
+
self.base_url = base_url
|
|
35
|
+
self.server_name = server_name
|
|
36
|
+
self.auth = auth
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self.tools = spec_to_tools(spec)
|
|
39
|
+
self.by_name = {tool["name"]: tool for tool in self.tools}
|
|
40
|
+
|
|
41
|
+
# -- public API used by the CLI --------------------------------------
|
|
42
|
+
@property
|
|
43
|
+
def tool_count(self) -> int:
|
|
44
|
+
return len(self.tools)
|
|
45
|
+
|
|
46
|
+
def public_tools(self) -> list[dict]:
|
|
47
|
+
return [
|
|
48
|
+
{k: v for k, v in tool.items() if not k.startswith("_")}
|
|
49
|
+
for tool in self.tools
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def call_tool(self, name: str, arguments: dict) -> tuple[str, bool]:
|
|
53
|
+
tool = self.by_name.get(name)
|
|
54
|
+
if tool is None:
|
|
55
|
+
raise KeyError(name)
|
|
56
|
+
request = build_request(self.base_url, tool["_meta"], arguments, self.auth)
|
|
57
|
+
if self.auth is not None:
|
|
58
|
+
request["url"] = self.auth.apply_query(request["url"])
|
|
59
|
+
result = execute(request, timeout=self.timeout)
|
|
60
|
+
return format_result(result)
|
|
61
|
+
|
|
62
|
+
# -- MCP plumbing -----------------------------------------------------
|
|
63
|
+
def _result(self, request_id: int | str | None, payload: dict) -> dict:
|
|
64
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": payload}
|
|
65
|
+
|
|
66
|
+
def _error(self, request_id: int | str | None, code: int, message: str) -> dict:
|
|
67
|
+
return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
|
|
68
|
+
|
|
69
|
+
def _text(self, text: str, is_error: bool = False) -> dict:
|
|
70
|
+
payload: dict = {"content": [{"type": "text", "text": text}]}
|
|
71
|
+
if is_error:
|
|
72
|
+
payload["isError"] = True
|
|
73
|
+
return payload
|
|
74
|
+
|
|
75
|
+
def handle_message(self, message: dict) -> dict | None:
|
|
76
|
+
method = message.get("method", "")
|
|
77
|
+
request_id = message.get("id")
|
|
78
|
+
params = message.get("params") or {}
|
|
79
|
+
|
|
80
|
+
if method == "initialize":
|
|
81
|
+
requested = params.get("protocolVersion", PROTOCOL_VERSION)
|
|
82
|
+
return self._result(
|
|
83
|
+
request_id,
|
|
84
|
+
{
|
|
85
|
+
"protocolVersion": requested,
|
|
86
|
+
"capabilities": {"tools": {}},
|
|
87
|
+
"serverInfo": {"name": self.server_name, "version": SERVER_VERSION},
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
if method.startswith("notifications/"):
|
|
91
|
+
return None
|
|
92
|
+
if method == "ping":
|
|
93
|
+
return self._result(request_id, {})
|
|
94
|
+
if method == "tools/list":
|
|
95
|
+
return self._result(request_id, {"tools": self.public_tools()})
|
|
96
|
+
if method == "tools/call":
|
|
97
|
+
name = params.get("name", "")
|
|
98
|
+
arguments = params.get("arguments") or {}
|
|
99
|
+
try:
|
|
100
|
+
text, is_error = self.call_tool(name, arguments)
|
|
101
|
+
except KeyError:
|
|
102
|
+
return self._error(request_id, -32601, f"unknown tool: {name}")
|
|
103
|
+
except RequestError as err:
|
|
104
|
+
return self._result(request_id, self._text(str(err), is_error=True))
|
|
105
|
+
return self._result(request_id, self._text(text, is_error=is_error))
|
|
106
|
+
return self._error(request_id, -32601, f"method not found: {method}")
|
|
107
|
+
|
|
108
|
+
def serve(self, stdin: Any = None, stdout: Any = None) -> None:
|
|
109
|
+
input_stream = stdin if stdin is not None else sys.stdin
|
|
110
|
+
output_stream = stdout if stdout is not None else sys.stdout
|
|
111
|
+
for line in input_stream:
|
|
112
|
+
line = line.strip()
|
|
113
|
+
if not line:
|
|
114
|
+
continue
|
|
115
|
+
decoded: Any
|
|
116
|
+
response: dict | None
|
|
117
|
+
try:
|
|
118
|
+
decoded = json.loads(line)
|
|
119
|
+
except json.JSONDecodeError:
|
|
120
|
+
response = self._error(None, -32700, "parse error")
|
|
121
|
+
else:
|
|
122
|
+
response = self.handle_message(decoded)
|
|
123
|
+
if response is not None:
|
|
124
|
+
output_stream.write(json.dumps(response, ensure_ascii=False) + "\n")
|
|
125
|
+
output_stream.flush()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def serve(
|
|
129
|
+
spec_path: str,
|
|
130
|
+
base_url: str,
|
|
131
|
+
name: str = "mcpify",
|
|
132
|
+
auth: AuthConfig | None = None,
|
|
133
|
+
timeout: float = 30.0,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Load the spec and block on the stdio loop (the `mcpify serve` entry)."""
|
|
136
|
+
from .spec import load_spec
|
|
137
|
+
|
|
138
|
+
spec = load_spec(spec_path)
|
|
139
|
+
ApiServer(spec, base_url, server_name=name, auth=auth, timeout=timeout).serve()
|