mcp-openapi-proxy 0.2.0__tar.gz → 0.3.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.
Files changed (19) hide show
  1. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/PKG-INFO +74 -12
  2. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/README.md +73 -11
  3. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/server_lowlevel.py +9 -6
  4. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/utils.py +25 -3
  5. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy.egg-info/PKG-INFO +74 -12
  6. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/pyproject.toml +1 -1
  7. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/LICENSE +0 -0
  8. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/__init__.py +0 -0
  9. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/handlers.py +0 -0
  10. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/logging_setup.py +0 -0
  11. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/openapi.py +0 -0
  12. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/server_fastmcp.py +0 -0
  13. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy/types.py +0 -0
  14. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy.egg-info/SOURCES.txt +0 -0
  15. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy.egg-info/dependency_links.txt +0 -0
  16. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy.egg-info/entry_points.txt +0 -0
  17. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy.egg-info/requires.txt +0 -0
  18. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/mcp_openapi_proxy.egg-info/top_level.txt +0 -0
  19. {mcp_openapi_proxy-0.2.0 → mcp_openapi_proxy-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-openapi-proxy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: MCP server for exposing OpenAPI specifications as MCP tools.
5
5
  Author-email: Matthew Hand <11550632+matthewhand@users.noreply.github.com>
6
6
  Requires-Python: >=3.10
@@ -31,10 +31,11 @@ Dynamic: license-file
31
31
  - ✅ **Codex**, **Gemini**, **Qwen**, **Kilocode**, **opencode** — native tool calls over stdio
32
32
  - ✅ **Vibe** — native discovery and read calls (writes were CLI-flaky, not a proxy issue)
33
33
  - ✅ **Letta** — Cloud (via a remote streamable-HTTP MCP URL) and self-hosted (via stdio)
34
- - ⚠️ **agy** — could not attach in headless mode; MCP tool enablement is interactive-only in agy (an agy limitation, not the proxy)
35
34
 
36
35
  See the [client matrix](#client-matrix) for attach mechanisms, models, and exact results.
37
36
 
37
+ > 📄 Full write-up: [**Verification case study**](docs/verification-case-study.md) — what the proxy is, the API + client matrices, and every defect found & fixed.
38
+
38
39
  **Prompts and resources are real now — including custom resources.** Both MCP surfaces are functional and tested: the `summarize_spec` / `whimsical_blog` prompts and the `spec_file` resource, plus a new `ADDITIONAL_RESOURCES` env var that serves your *own* use-case documents (e.g. a NetBox naming policy or an Asana project-layout guide) as MCP resources — see `examples/resources/`.
39
40
 
40
41
  **Bug fixes (every one live-verified):**
@@ -63,7 +64,7 @@ Full environment-variable reference is in [Environment Variables](#environment-v
63
64
  - [Environment Variables](#environment-variables)
64
65
  - [Verified Clients & Live Results (2026-06-12)](#verified-clients--live-results-2026-06-12)
65
66
  - [Tips](#tips)
66
- - [Examples](#examples) — Glama, Fly.io, Render, Slack, GetZep, Virustotal, Notion, Asana, APIs.guru, NetBox, Box, WolframAlpha (collapsed)
67
+ - [Examples](#examples) — Glama, Fly.io, Render, Slack, GetZep, Virustotal, Notion, Asana, APIs.guru, NetBox, Box, WolframAlpha, WordPress (collapsed)
67
68
  - [Troubleshooting](#troubleshooting)
68
69
  - [License](#license)
69
70
 
@@ -174,15 +175,27 @@ The example configurations below were exercised against the live APIs, and the p
174
175
 
175
176
  | Agent CLI | Model used (live test) | MCP attach mechanism | Tool calls | Prompts/Resources surfaced to model? |
176
177
  |---|---|---|---|---|
177
- | Codex | `gpt-5-codex` (OpenAI API) | `codex exec -c mcp_servers.*` | ✅ native | ❌ (used raw stdio) |
178
- | Gemini | Google OAuth free tier (CLI default model) | project `.gemini/settings.json` `mcpServers` | ✅ native | ❌ interactive slash-commands only |
179
- | Qwen | `agent` group via local LiteLLM gateway | project `.qwen/settings.json` | ✅ native | ❌ NO_PROMPT_ACCESS |
180
- | Kilocode | `kilo-auto/free` | global `settings/mcp_settings.json`, clean workspace | ✅ native | ❌ |
181
- | opencode | `orchestration` group via local LiteLLM gateway | `~/.config/opencode/opencode.json` `mcp` | ✅ native | ❌ |
182
- | Vibe | `mistral-medium-3.5` | `~/.vibe/config.toml` `[[mcp_servers]]` | ✅ discovery + reads (writes flaky) | ❌ |
183
- | agy | — | — | ❌ headless cannot enable MCP | — |
184
- | letta cloud | Letta Cloud default | streamable-HTTP MCP URL (`/mcp add --transport http` + bearer) | ✅ remote (stdio rejected) | — |
185
- | letta (self-hosted ≤0.11.x) | `agent` group via local LiteLLM gateway | stdio via `PUT /v1/tools/mcp/servers` | ✅ native | — |
178
+ | Codex | `gpt-5-codex` (OpenAI API) | `codex exec -c mcp_servers.*` | ✅ native | unknown ‡ |
179
+ | Gemini | Google OAuth free tier (CLI default model) | project `.gemini/settings.json` `mcpServers` | ✅ native | prompts: interactive slash only · resources: ❌ † |
180
+ | Qwen | `agent` group via local LiteLLM gateway | project `.qwen/settings.json` | ✅ native | prompts: ✅ (slash `/summarize_spec`) · resources: ❌ † |
181
+ | Kilocode | `kilo-auto/free` | global `settings/mcp_settings.json`, clean workspace | ✅ native | prompts: ❌ · resources: ✅ (`access_mcp_resource`) † |
182
+ | opencode | `orchestration` group via local LiteLLM gateway | `~/.config/opencode/opencode.json` `mcp` | ✅ native | unknown ‡ |
183
+ | Vibe | `mistral-medium-3.5` | `~/.vibe/config.toml` `[[mcp_servers]]` | ✅ discovery + reads (writes flaky) | prompts: ❌ · resources: ❌ (tools-only) † |
184
+ | agy | — | — | ❌ headless cannot enable MCP | n/a |
185
+ | letta cloud | Letta Cloud default | streamable-HTTP MCP URL (`/mcp add --transport http` + bearer) | ✅ remote (stdio rejected) | unknown ‡ |
186
+ | letta (self-hosted ≤0.11.x) | `agent` group via local LiteLLM gateway | stdio via `PUT /v1/tools/mcp/servers` | ✅ native | unknown ‡ |
187
+
188
+ > **Prompts/resources column — read this.** The original 0.2.0 sweep ran while the
189
+ > server advertised prompts/resources **only when `ENABLE_PROMPTS`/`ENABLE_RESOURCES`
190
+ > were set — they defaulted OFF.** Per the MCP spec a client won't call
191
+ > `prompts/list`/`resources/list` unless the capability is advertised, so those early
192
+ > results measured the *server's* default, not the clients. **All prior prompt/resource
193
+ > findings are therefore voided.** This release defaults advertising **on**.
194
+ >
195
+ > - **† re-verified 2026-06-14** with advertising on, against the real client binary. Genuine, uneven: **tools** everywhere; **prompts→model** Qwen (slash) & Gemini (interactive only); **resources→model** Kilocode. Vibe is tools-only (confirmed in source).
196
+ > - **‡ unknown** — not yet re-tested under advertising-on (opencode not installed here; Letta needs a running server + gateway; Codex parked). Prior ❌ values are not carried forward.
197
+ >
198
+ > Tool-calling (the `Tool calls` column) was unaffected by the advertising default and stands as originally verified.
186
199
 
187
200
  Minimal sanitized configs per client (the no-auth Glama spec is used as the smallest working example; substitute your own spec URL and `$YOUR_KEY` as needed):
188
201
 
@@ -868,6 +881,55 @@ You can now use the MCP ecosystem to list and invoke WolframAlpha API tools. For
868
881
 
869
882
  </details>
870
883
 
884
+ <details>
885
+ <summary><b>Letta (self-hosted + Cloud)</b> — agent platform; stdio for self-hosted, authenticated remote HTTP for Cloud</summary>
886
+
887
+ Letta runs agents on a server, so attachment differs by deployment:
888
+
889
+ - **Self-hosted Letta** accepts a **stdio** MCP server via `PUT /v1/tools/mcp/servers` — no network exposure.
890
+ - **Letta Cloud** rejects stdio and needs a **remote streamable-HTTP** MCP URL behind authentication.
891
+
892
+ Both were verified live (an agent autonomously called `get_v1_attributes` from the Glama spec through the proxy). Full setup for both paths, including the supergateway wrapper and the security note for the Cloud endpoint, is in [`examples/letta/README.md`](examples/letta/README.md).
893
+
894
+ </details>
895
+
896
+ <details>
897
+ <summary><b>WordPress Example</b> — publish to a real WordPress blog from any MCP agent</summary>
898
+
899
+ This example turns the WordPress REST API into MCP tools so an agent can **create, read, update, and trash blog posts**. It is the example behind a real-world demonstration: a fleet of agent CLIs published to the live blog at **[matthewhand.mywebcommunity.org](https://matthewhand.mywebcommunity.org/)** through this proxy — see the write-up [*“Turning Any API into Agent Tools”*](https://matthewhand.mywebcommunity.org/2026/06/12/turning-any-api-into-agent-tools-a-real-world-test-of-mcp-openapi-proxy/), itself posted via the proxy.
900
+
901
+ Example config — `examples/wordpress-claude_desktop_config.json`:
902
+
903
+ ```json
904
+ {
905
+ "mcpServers": {
906
+ "wordpress": {
907
+ "command": "uvx",
908
+ "args": ["mcp-openapi-proxy"],
909
+ "env": {
910
+ "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/matthewhand/mcp-openapi-proxy/main/examples/wordpress-openapi.json",
911
+ "SERVER_URL_OVERRIDE": "https://your-site.example.com/wp-json",
912
+ "EXTRA_HEADERS": "Authorization: Basic BASE64_OF_USERNAME_COLON_APPLICATION_PASSWORD",
913
+ "IGNORE_SSL_TOOLS": "false",
914
+ "TOOL_WHITELIST": "/wp/v2/posts"
915
+ }
916
+ }
917
+ }
918
+ }
919
+ ```
920
+
921
+ Exposed tools (from the bundled `examples/wordpress-openapi.json`): `get_wp_v2_posts`, `post_wp_v2_posts` (create), `get_wp_v2_posts_by_id`, `post_wp_v2_posts_by_id` (update), and `delete_wp_v2_posts_by_id` (trash) — the full post lifecycle.
922
+
923
+ **Auth — use a WordPress *Application Password*, not your login password.** WordPress REST rejects login passwords for Basic auth. In `wp-admin → Users → (your user) → Application Passwords`, create one, then base64-encode `username:application-password` for the `EXTRA_HEADERS` value:
924
+
925
+ ```bash
926
+ printf 'myuser:xxxx xxxx xxxx xxxx xxxx xxxx' | base64 -w0
927
+ ```
928
+
929
+ Tips: set `IGNORE_SSL_TOOLS=true` only if your host serves a self-signed/mismatched cert; give each agent its own (revocable) application password; keep `TOOL_WHITELIST=/wp/v2/posts` so the agent can touch only posts.
930
+
931
+ </details>
932
+
871
933
  ## Troubleshooting
872
934
 
873
935
  ### JSON-RPC Testing
@@ -9,10 +9,11 @@
9
9
  - ✅ **Codex**, **Gemini**, **Qwen**, **Kilocode**, **opencode** — native tool calls over stdio
10
10
  - ✅ **Vibe** — native discovery and read calls (writes were CLI-flaky, not a proxy issue)
11
11
  - ✅ **Letta** — Cloud (via a remote streamable-HTTP MCP URL) and self-hosted (via stdio)
12
- - ⚠️ **agy** — could not attach in headless mode; MCP tool enablement is interactive-only in agy (an agy limitation, not the proxy)
13
12
 
14
13
  See the [client matrix](#client-matrix) for attach mechanisms, models, and exact results.
15
14
 
15
+ > 📄 Full write-up: [**Verification case study**](docs/verification-case-study.md) — what the proxy is, the API + client matrices, and every defect found & fixed.
16
+
16
17
  **Prompts and resources are real now — including custom resources.** Both MCP surfaces are functional and tested: the `summarize_spec` / `whimsical_blog` prompts and the `spec_file` resource, plus a new `ADDITIONAL_RESOURCES` env var that serves your *own* use-case documents (e.g. a NetBox naming policy or an Asana project-layout guide) as MCP resources — see `examples/resources/`.
17
18
 
18
19
  **Bug fixes (every one live-verified):**
@@ -41,7 +42,7 @@ Full environment-variable reference is in [Environment Variables](#environment-v
41
42
  - [Environment Variables](#environment-variables)
42
43
  - [Verified Clients & Live Results (2026-06-12)](#verified-clients--live-results-2026-06-12)
43
44
  - [Tips](#tips)
44
- - [Examples](#examples) — Glama, Fly.io, Render, Slack, GetZep, Virustotal, Notion, Asana, APIs.guru, NetBox, Box, WolframAlpha (collapsed)
45
+ - [Examples](#examples) — Glama, Fly.io, Render, Slack, GetZep, Virustotal, Notion, Asana, APIs.guru, NetBox, Box, WolframAlpha, WordPress (collapsed)
45
46
  - [Troubleshooting](#troubleshooting)
46
47
  - [License](#license)
47
48
 
@@ -152,15 +153,27 @@ The example configurations below were exercised against the live APIs, and the p
152
153
 
153
154
  | Agent CLI | Model used (live test) | MCP attach mechanism | Tool calls | Prompts/Resources surfaced to model? |
154
155
  |---|---|---|---|---|
155
- | Codex | `gpt-5-codex` (OpenAI API) | `codex exec -c mcp_servers.*` | ✅ native | ❌ (used raw stdio) |
156
- | Gemini | Google OAuth free tier (CLI default model) | project `.gemini/settings.json` `mcpServers` | ✅ native | ❌ interactive slash-commands only |
157
- | Qwen | `agent` group via local LiteLLM gateway | project `.qwen/settings.json` | ✅ native | ❌ NO_PROMPT_ACCESS |
158
- | Kilocode | `kilo-auto/free` | global `settings/mcp_settings.json`, clean workspace | ✅ native | ❌ |
159
- | opencode | `orchestration` group via local LiteLLM gateway | `~/.config/opencode/opencode.json` `mcp` | ✅ native | ❌ |
160
- | Vibe | `mistral-medium-3.5` | `~/.vibe/config.toml` `[[mcp_servers]]` | ✅ discovery + reads (writes flaky) | ❌ |
161
- | agy | — | — | ❌ headless cannot enable MCP | — |
162
- | letta cloud | Letta Cloud default | streamable-HTTP MCP URL (`/mcp add --transport http` + bearer) | ✅ remote (stdio rejected) | — |
163
- | letta (self-hosted ≤0.11.x) | `agent` group via local LiteLLM gateway | stdio via `PUT /v1/tools/mcp/servers` | ✅ native | — |
156
+ | Codex | `gpt-5-codex` (OpenAI API) | `codex exec -c mcp_servers.*` | ✅ native | unknown ‡ |
157
+ | Gemini | Google OAuth free tier (CLI default model) | project `.gemini/settings.json` `mcpServers` | ✅ native | prompts: interactive slash only · resources: ❌ † |
158
+ | Qwen | `agent` group via local LiteLLM gateway | project `.qwen/settings.json` | ✅ native | prompts: ✅ (slash `/summarize_spec`) · resources: ❌ † |
159
+ | Kilocode | `kilo-auto/free` | global `settings/mcp_settings.json`, clean workspace | ✅ native | prompts: ❌ · resources: ✅ (`access_mcp_resource`) † |
160
+ | opencode | `orchestration` group via local LiteLLM gateway | `~/.config/opencode/opencode.json` `mcp` | ✅ native | unknown ‡ |
161
+ | Vibe | `mistral-medium-3.5` | `~/.vibe/config.toml` `[[mcp_servers]]` | ✅ discovery + reads (writes flaky) | prompts: ❌ · resources: ❌ (tools-only) † |
162
+ | agy | — | — | ❌ headless cannot enable MCP | n/a |
163
+ | letta cloud | Letta Cloud default | streamable-HTTP MCP URL (`/mcp add --transport http` + bearer) | ✅ remote (stdio rejected) | unknown ‡ |
164
+ | letta (self-hosted ≤0.11.x) | `agent` group via local LiteLLM gateway | stdio via `PUT /v1/tools/mcp/servers` | ✅ native | unknown ‡ |
165
+
166
+ > **Prompts/resources column — read this.** The original 0.2.0 sweep ran while the
167
+ > server advertised prompts/resources **only when `ENABLE_PROMPTS`/`ENABLE_RESOURCES`
168
+ > were set — they defaulted OFF.** Per the MCP spec a client won't call
169
+ > `prompts/list`/`resources/list` unless the capability is advertised, so those early
170
+ > results measured the *server's* default, not the clients. **All prior prompt/resource
171
+ > findings are therefore voided.** This release defaults advertising **on**.
172
+ >
173
+ > - **† re-verified 2026-06-14** with advertising on, against the real client binary. Genuine, uneven: **tools** everywhere; **prompts→model** Qwen (slash) & Gemini (interactive only); **resources→model** Kilocode. Vibe is tools-only (confirmed in source).
174
+ > - **‡ unknown** — not yet re-tested under advertising-on (opencode not installed here; Letta needs a running server + gateway; Codex parked). Prior ❌ values are not carried forward.
175
+ >
176
+ > Tool-calling (the `Tool calls` column) was unaffected by the advertising default and stands as originally verified.
164
177
 
165
178
  Minimal sanitized configs per client (the no-auth Glama spec is used as the smallest working example; substitute your own spec URL and `$YOUR_KEY` as needed):
166
179
 
@@ -846,6 +859,55 @@ You can now use the MCP ecosystem to list and invoke WolframAlpha API tools. For
846
859
 
847
860
  </details>
848
861
 
862
+ <details>
863
+ <summary><b>Letta (self-hosted + Cloud)</b> — agent platform; stdio for self-hosted, authenticated remote HTTP for Cloud</summary>
864
+
865
+ Letta runs agents on a server, so attachment differs by deployment:
866
+
867
+ - **Self-hosted Letta** accepts a **stdio** MCP server via `PUT /v1/tools/mcp/servers` — no network exposure.
868
+ - **Letta Cloud** rejects stdio and needs a **remote streamable-HTTP** MCP URL behind authentication.
869
+
870
+ Both were verified live (an agent autonomously called `get_v1_attributes` from the Glama spec through the proxy). Full setup for both paths, including the supergateway wrapper and the security note for the Cloud endpoint, is in [`examples/letta/README.md`](examples/letta/README.md).
871
+
872
+ </details>
873
+
874
+ <details>
875
+ <summary><b>WordPress Example</b> — publish to a real WordPress blog from any MCP agent</summary>
876
+
877
+ This example turns the WordPress REST API into MCP tools so an agent can **create, read, update, and trash blog posts**. It is the example behind a real-world demonstration: a fleet of agent CLIs published to the live blog at **[matthewhand.mywebcommunity.org](https://matthewhand.mywebcommunity.org/)** through this proxy — see the write-up [*“Turning Any API into Agent Tools”*](https://matthewhand.mywebcommunity.org/2026/06/12/turning-any-api-into-agent-tools-a-real-world-test-of-mcp-openapi-proxy/), itself posted via the proxy.
878
+
879
+ Example config — `examples/wordpress-claude_desktop_config.json`:
880
+
881
+ ```json
882
+ {
883
+ "mcpServers": {
884
+ "wordpress": {
885
+ "command": "uvx",
886
+ "args": ["mcp-openapi-proxy"],
887
+ "env": {
888
+ "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/matthewhand/mcp-openapi-proxy/main/examples/wordpress-openapi.json",
889
+ "SERVER_URL_OVERRIDE": "https://your-site.example.com/wp-json",
890
+ "EXTRA_HEADERS": "Authorization: Basic BASE64_OF_USERNAME_COLON_APPLICATION_PASSWORD",
891
+ "IGNORE_SSL_TOOLS": "false",
892
+ "TOOL_WHITELIST": "/wp/v2/posts"
893
+ }
894
+ }
895
+ }
896
+ }
897
+ ```
898
+
899
+ Exposed tools (from the bundled `examples/wordpress-openapi.json`): `get_wp_v2_posts`, `post_wp_v2_posts` (create), `get_wp_v2_posts_by_id`, `post_wp_v2_posts_by_id` (update), and `delete_wp_v2_posts_by_id` (trash) — the full post lifecycle.
900
+
901
+ **Auth — use a WordPress *Application Password*, not your login password.** WordPress REST rejects login passwords for Basic auth. In `wp-admin → Users → (your user) → Application Passwords`, create one, then base64-encode `username:application-password` for the `EXTRA_HEADERS` value:
902
+
903
+ ```bash
904
+ printf 'myuser:xxxx xxxx xxxx xxxx xxxx xxxx' | base64 -w0
905
+ ```
906
+
907
+ Tips: set `IGNORE_SSL_TOOLS=true` only if your host serves a self-signed/mismatched cert; give each agent its own (revocable) application password; keep `TOOL_WHITELIST=/wp/v2/posts` so the agent can touch only posts.
908
+
909
+ </details>
910
+
849
911
  ## Troubleshooting
850
912
 
851
913
  ### JSON-RPC Testing
@@ -14,8 +14,8 @@ Configuration is controlled via environment variables:
14
14
  - CAPABILITIES_RESOURCES: Set to "true" to enable resources advertising (default: false).
15
15
  - CAPABILITIES_PROMPTS: Set to "true" to enable prompts advertising (default: false).
16
16
  - ENABLE_TOOLS: Set to "false" to disable tools functionality (default: true).
17
- - ENABLE_RESOURCES: Set to "true" to enable resources functionality (default: false).
18
- - ENABLE_PROMPTS: Set to "true" to enable prompts functionality (default: false).
17
+ - ENABLE_RESOURCES: Set to "false" to disable resources functionality (default: true).
18
+ - ENABLE_PROMPTS: Set to "false" to disable prompts functionality (default: true).
19
19
  """
20
20
 
21
21
  import os
@@ -53,10 +53,13 @@ CAPABILITIES_TOOLS = os.getenv("CAPABILITIES_TOOLS", "false").lower() == "true"
53
53
  CAPABILITIES_RESOURCES = os.getenv("CAPABILITIES_RESOURCES", "false").lower() == "true"
54
54
  CAPABILITIES_PROMPTS = os.getenv("CAPABILITIES_PROMPTS", "false").lower() == "true"
55
55
 
56
- # Check feature enablement envvars (tools on, others off by default)
56
+ # Feature enablement (all on by default). Advertising a capability is
57
+ # required for clients to even attempt prompts/list & resources/list;
58
+ # defaulting these OFF made prompts/resources invisible to every client
59
+ # despite being implemented. Set to "false" to opt out.
57
60
  ENABLE_TOOLS = os.getenv("ENABLE_TOOLS", "true").lower() == "true"
58
- ENABLE_RESOURCES = os.getenv("ENABLE_RESOURCES", "false").lower() == "true"
59
- ENABLE_PROMPTS = os.getenv("ENABLE_PROMPTS", "false").lower() == "true"
61
+ ENABLE_RESOURCES = os.getenv("ENABLE_RESOURCES", "true").lower() == "true"
62
+ ENABLE_PROMPTS = os.getenv("ENABLE_PROMPTS", "true").lower() == "true"
60
63
 
61
64
  # Resource and prompt DEFINITIONS are always present so the feature is
62
65
  # deterministically testable. Whether they are EXPOSED to clients is gated
@@ -408,7 +411,7 @@ async def read_resource(request: types.ReadResourceRequest) -> types.ReadResourc
408
411
  ]
409
412
  )
410
413
  logger.debug("Dumping spec to JSON...")
411
- spec_json = json.dumps(spec_data, indent=2)
414
+ spec_json = json.dumps(spec_data, indent=2, default=str)
412
415
  logger.debug(f"Forcing spec JSON return: {spec_json[:50]}...")
413
416
  return types.ReadResourceResult(
414
417
  contents=[
@@ -14,6 +14,28 @@ from mcp import types
14
14
  # Import the configured logger
15
15
  from .logging_setup import logger
16
16
 
17
+
18
+ class _NoTimestampSafeLoader(yaml.SafeLoader):
19
+ """SafeLoader that keeps unquoted RFC3339 timestamps as plain strings.
20
+
21
+ PyYAML's SafeLoader auto-converts values like ``2015-02-22T20:00:45.000Z``
22
+ into Python ``datetime`` objects. Such objects are not JSON-serializable,
23
+ which crashed spec caching and ``resources/read`` for specs that carry
24
+ datetime example values (e.g. the apis.guru directory spec). Loading specs
25
+ with this loader keeps every scalar JSON-safe.
26
+ """
27
+
28
+
29
+ _NoTimestampSafeLoader.yaml_implicit_resolvers = {
30
+ key: [(tag, regexp) for tag, regexp in resolvers if tag != "tag:yaml.org,2002:timestamp"]
31
+ for key, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items()
32
+ }
33
+
34
+
35
+ def yaml_load_safe(content: str):
36
+ """Parse YAML with timestamps preserved as strings (JSON-safe)."""
37
+ return yaml.load(content, Loader=_NoTimestampSafeLoader)
38
+
17
39
  def setup_logging(debug: bool = False):
18
40
  """
19
41
  Configure logging for the application.
@@ -196,7 +218,7 @@ def _spec_cache_store(url: str, spec: Dict) -> None:
196
218
  try:
197
219
  path = _spec_cache_path(url)
198
220
  with open(path + ".tmp", "w") as f:
199
- json.dump(spec, f)
221
+ json.dump(spec, f, default=str)
200
222
  os.replace(path + ".tmp", path)
201
223
  logger.debug(f"Cached spec for {url} at {path}")
202
224
  except OSError as exc:
@@ -226,7 +248,7 @@ def fetch_openapi_spec(url: str, retries: int = 3) -> Optional[Dict]:
226
248
  logger.debug(f"Using {spec_format.upper()} parser based on OPENAPI_SPEC_FORMAT env var")
227
249
  if spec_format == "yaml":
228
250
  try:
229
- spec = yaml.safe_load(content)
251
+ spec = yaml_load_safe(content)
230
252
  logger.debug(f"Parsed as YAML from {url}")
231
253
  except yaml.YAMLError as ye:
232
254
  logger.error(f"YAML parsing failed: {ye}. Raw content: {content[:500]}...")
@@ -252,7 +274,7 @@ def fetch_openapi_spec(url: str, retries: int = 3) -> Optional[Dict]:
252
274
  logger.debug(f"Parsed as JSON from {url}")
253
275
  except json.JSONDecodeError:
254
276
  try:
255
- spec = yaml.safe_load(content)
277
+ spec = yaml_load_safe(content)
256
278
  logger.debug(f"Parsed as YAML from {url}")
257
279
  except yaml.YAMLError as ye:
258
280
  logger.error(f"YAML parsing failed: {ye}. Raw content: {content[:500]}...")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-openapi-proxy
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: MCP server for exposing OpenAPI specifications as MCP tools.
5
5
  Author-email: Matthew Hand <11550632+matthewhand@users.noreply.github.com>
6
6
  Requires-Python: >=3.10
@@ -31,10 +31,11 @@ Dynamic: license-file
31
31
  - ✅ **Codex**, **Gemini**, **Qwen**, **Kilocode**, **opencode** — native tool calls over stdio
32
32
  - ✅ **Vibe** — native discovery and read calls (writes were CLI-flaky, not a proxy issue)
33
33
  - ✅ **Letta** — Cloud (via a remote streamable-HTTP MCP URL) and self-hosted (via stdio)
34
- - ⚠️ **agy** — could not attach in headless mode; MCP tool enablement is interactive-only in agy (an agy limitation, not the proxy)
35
34
 
36
35
  See the [client matrix](#client-matrix) for attach mechanisms, models, and exact results.
37
36
 
37
+ > 📄 Full write-up: [**Verification case study**](docs/verification-case-study.md) — what the proxy is, the API + client matrices, and every defect found & fixed.
38
+
38
39
  **Prompts and resources are real now — including custom resources.** Both MCP surfaces are functional and tested: the `summarize_spec` / `whimsical_blog` prompts and the `spec_file` resource, plus a new `ADDITIONAL_RESOURCES` env var that serves your *own* use-case documents (e.g. a NetBox naming policy or an Asana project-layout guide) as MCP resources — see `examples/resources/`.
39
40
 
40
41
  **Bug fixes (every one live-verified):**
@@ -63,7 +64,7 @@ Full environment-variable reference is in [Environment Variables](#environment-v
63
64
  - [Environment Variables](#environment-variables)
64
65
  - [Verified Clients & Live Results (2026-06-12)](#verified-clients--live-results-2026-06-12)
65
66
  - [Tips](#tips)
66
- - [Examples](#examples) — Glama, Fly.io, Render, Slack, GetZep, Virustotal, Notion, Asana, APIs.guru, NetBox, Box, WolframAlpha (collapsed)
67
+ - [Examples](#examples) — Glama, Fly.io, Render, Slack, GetZep, Virustotal, Notion, Asana, APIs.guru, NetBox, Box, WolframAlpha, WordPress (collapsed)
67
68
  - [Troubleshooting](#troubleshooting)
68
69
  - [License](#license)
69
70
 
@@ -174,15 +175,27 @@ The example configurations below were exercised against the live APIs, and the p
174
175
 
175
176
  | Agent CLI | Model used (live test) | MCP attach mechanism | Tool calls | Prompts/Resources surfaced to model? |
176
177
  |---|---|---|---|---|
177
- | Codex | `gpt-5-codex` (OpenAI API) | `codex exec -c mcp_servers.*` | ✅ native | ❌ (used raw stdio) |
178
- | Gemini | Google OAuth free tier (CLI default model) | project `.gemini/settings.json` `mcpServers` | ✅ native | ❌ interactive slash-commands only |
179
- | Qwen | `agent` group via local LiteLLM gateway | project `.qwen/settings.json` | ✅ native | ❌ NO_PROMPT_ACCESS |
180
- | Kilocode | `kilo-auto/free` | global `settings/mcp_settings.json`, clean workspace | ✅ native | ❌ |
181
- | opencode | `orchestration` group via local LiteLLM gateway | `~/.config/opencode/opencode.json` `mcp` | ✅ native | ❌ |
182
- | Vibe | `mistral-medium-3.5` | `~/.vibe/config.toml` `[[mcp_servers]]` | ✅ discovery + reads (writes flaky) | ❌ |
183
- | agy | — | — | ❌ headless cannot enable MCP | — |
184
- | letta cloud | Letta Cloud default | streamable-HTTP MCP URL (`/mcp add --transport http` + bearer) | ✅ remote (stdio rejected) | — |
185
- | letta (self-hosted ≤0.11.x) | `agent` group via local LiteLLM gateway | stdio via `PUT /v1/tools/mcp/servers` | ✅ native | — |
178
+ | Codex | `gpt-5-codex` (OpenAI API) | `codex exec -c mcp_servers.*` | ✅ native | unknown ‡ |
179
+ | Gemini | Google OAuth free tier (CLI default model) | project `.gemini/settings.json` `mcpServers` | ✅ native | prompts: interactive slash only · resources: ❌ † |
180
+ | Qwen | `agent` group via local LiteLLM gateway | project `.qwen/settings.json` | ✅ native | prompts: ✅ (slash `/summarize_spec`) · resources: ❌ † |
181
+ | Kilocode | `kilo-auto/free` | global `settings/mcp_settings.json`, clean workspace | ✅ native | prompts: ❌ · resources: ✅ (`access_mcp_resource`) † |
182
+ | opencode | `orchestration` group via local LiteLLM gateway | `~/.config/opencode/opencode.json` `mcp` | ✅ native | unknown ‡ |
183
+ | Vibe | `mistral-medium-3.5` | `~/.vibe/config.toml` `[[mcp_servers]]` | ✅ discovery + reads (writes flaky) | prompts: ❌ · resources: ❌ (tools-only) † |
184
+ | agy | — | — | ❌ headless cannot enable MCP | n/a |
185
+ | letta cloud | Letta Cloud default | streamable-HTTP MCP URL (`/mcp add --transport http` + bearer) | ✅ remote (stdio rejected) | unknown ‡ |
186
+ | letta (self-hosted ≤0.11.x) | `agent` group via local LiteLLM gateway | stdio via `PUT /v1/tools/mcp/servers` | ✅ native | unknown ‡ |
187
+
188
+ > **Prompts/resources column — read this.** The original 0.2.0 sweep ran while the
189
+ > server advertised prompts/resources **only when `ENABLE_PROMPTS`/`ENABLE_RESOURCES`
190
+ > were set — they defaulted OFF.** Per the MCP spec a client won't call
191
+ > `prompts/list`/`resources/list` unless the capability is advertised, so those early
192
+ > results measured the *server's* default, not the clients. **All prior prompt/resource
193
+ > findings are therefore voided.** This release defaults advertising **on**.
194
+ >
195
+ > - **† re-verified 2026-06-14** with advertising on, against the real client binary. Genuine, uneven: **tools** everywhere; **prompts→model** Qwen (slash) & Gemini (interactive only); **resources→model** Kilocode. Vibe is tools-only (confirmed in source).
196
+ > - **‡ unknown** — not yet re-tested under advertising-on (opencode not installed here; Letta needs a running server + gateway; Codex parked). Prior ❌ values are not carried forward.
197
+ >
198
+ > Tool-calling (the `Tool calls` column) was unaffected by the advertising default and stands as originally verified.
186
199
 
187
200
  Minimal sanitized configs per client (the no-auth Glama spec is used as the smallest working example; substitute your own spec URL and `$YOUR_KEY` as needed):
188
201
 
@@ -868,6 +881,55 @@ You can now use the MCP ecosystem to list and invoke WolframAlpha API tools. For
868
881
 
869
882
  </details>
870
883
 
884
+ <details>
885
+ <summary><b>Letta (self-hosted + Cloud)</b> — agent platform; stdio for self-hosted, authenticated remote HTTP for Cloud</summary>
886
+
887
+ Letta runs agents on a server, so attachment differs by deployment:
888
+
889
+ - **Self-hosted Letta** accepts a **stdio** MCP server via `PUT /v1/tools/mcp/servers` — no network exposure.
890
+ - **Letta Cloud** rejects stdio and needs a **remote streamable-HTTP** MCP URL behind authentication.
891
+
892
+ Both were verified live (an agent autonomously called `get_v1_attributes` from the Glama spec through the proxy). Full setup for both paths, including the supergateway wrapper and the security note for the Cloud endpoint, is in [`examples/letta/README.md`](examples/letta/README.md).
893
+
894
+ </details>
895
+
896
+ <details>
897
+ <summary><b>WordPress Example</b> — publish to a real WordPress blog from any MCP agent</summary>
898
+
899
+ This example turns the WordPress REST API into MCP tools so an agent can **create, read, update, and trash blog posts**. It is the example behind a real-world demonstration: a fleet of agent CLIs published to the live blog at **[matthewhand.mywebcommunity.org](https://matthewhand.mywebcommunity.org/)** through this proxy — see the write-up [*“Turning Any API into Agent Tools”*](https://matthewhand.mywebcommunity.org/2026/06/12/turning-any-api-into-agent-tools-a-real-world-test-of-mcp-openapi-proxy/), itself posted via the proxy.
900
+
901
+ Example config — `examples/wordpress-claude_desktop_config.json`:
902
+
903
+ ```json
904
+ {
905
+ "mcpServers": {
906
+ "wordpress": {
907
+ "command": "uvx",
908
+ "args": ["mcp-openapi-proxy"],
909
+ "env": {
910
+ "OPENAPI_SPEC_URL": "https://raw.githubusercontent.com/matthewhand/mcp-openapi-proxy/main/examples/wordpress-openapi.json",
911
+ "SERVER_URL_OVERRIDE": "https://your-site.example.com/wp-json",
912
+ "EXTRA_HEADERS": "Authorization: Basic BASE64_OF_USERNAME_COLON_APPLICATION_PASSWORD",
913
+ "IGNORE_SSL_TOOLS": "false",
914
+ "TOOL_WHITELIST": "/wp/v2/posts"
915
+ }
916
+ }
917
+ }
918
+ }
919
+ ```
920
+
921
+ Exposed tools (from the bundled `examples/wordpress-openapi.json`): `get_wp_v2_posts`, `post_wp_v2_posts` (create), `get_wp_v2_posts_by_id`, `post_wp_v2_posts_by_id` (update), and `delete_wp_v2_posts_by_id` (trash) — the full post lifecycle.
922
+
923
+ **Auth — use a WordPress *Application Password*, not your login password.** WordPress REST rejects login passwords for Basic auth. In `wp-admin → Users → (your user) → Application Passwords`, create one, then base64-encode `username:application-password` for the `EXTRA_HEADERS` value:
924
+
925
+ ```bash
926
+ printf 'myuser:xxxx xxxx xxxx xxxx xxxx xxxx' | base64 -w0
927
+ ```
928
+
929
+ Tips: set `IGNORE_SSL_TOOLS=true` only if your host serves a self-signed/mismatched cert; give each agent its own (revocable) application password; keep `TOOL_WHITELIST=/wp/v2/posts` so the agent can touch only posts.
930
+
931
+ </details>
932
+
871
933
  ## Troubleshooting
872
934
 
873
935
  ### JSON-RPC Testing
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "mcp-openapi-proxy"
7
7
  requires-python = ">=3.10"
8
- version = "0.2.0"
8
+ version = "0.3.0"
9
9
  description = "MCP server for exposing OpenAPI specifications as MCP tools."
10
10
  readme = "README.md"
11
11
  authors = [