api-mcp-compiler 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.
- {api_mcp_compiler-0.2.0/src/api_mcp_compiler.egg-info → api_mcp_compiler-0.3.0}/PKG-INFO +39 -27
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/README.md +38 -26
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/pyproject.toml +1 -1
- api_mcp_compiler-0.3.0/src/api_mcp_compiler/__init__.py +1 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/mcp_server.py +88 -2
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/soap_server.py +51 -6
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0/src/api_mcp_compiler.egg-info}/PKG-INFO +39 -27
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/SOURCES.txt +1 -0
- api_mcp_compiler-0.3.0/tests/test_generated_retry.py +263 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_mcp_server.py +9 -4
- api_mcp_compiler-0.2.0/src/api_mcp_compiler/__init__.py +0 -1
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/LICENSE +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/NOTICE +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/setup.cfg +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/benchmarks.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/cli.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/composite.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/credentials.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/schema.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/tools.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/contracts.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/harness.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/model_driver.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/oracles.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/preregistration.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/restbench.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/state.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/coverage.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/documents.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/openapi.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/refs.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/swagger2.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/wsdl.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/ingest/xsd.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/models.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/planning/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/planning/approval.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/planning/baseline.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/planning/overlay.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/planning/report.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/planning/semantic.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/policy/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/policy/synthesis.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/provenance.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/reporting/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/reporting/conversion_report.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/routes.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/runtime/__init__.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/runtime/governance.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/runtime/mock.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/api_semantic_ir.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/eval_corpus.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/evaluation_run.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/mcp_tool_surface.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/policy_manifest.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/preregistration.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/tool_overlay.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/tool_plan.schema.json +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/dependency_links.txt +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/entry_points.txt +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/requires.txt +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/top_level.txt +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_baseline_planner.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_benchmarks.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_codegen.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_codegen_schema.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_contracts.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_coverage.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_determinism.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_evaluation.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_generated_confirmation.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_generated_credentials.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_mock_runtime.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_openapi.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_packaging.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_policy.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_preregistration.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_provenance.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_refs.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_restbench.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_review_flow.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_semantic.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_soap_server.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_swagger2.py +0 -0
- {api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/tests/test_wsdl.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-mcp-compiler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Semantic and safe compilation of enterprise APIs into MCP tool surfaces
|
|
5
5
|
Author-email: Subhadip Mitra <contact@subhadipmitra.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -87,8 +87,9 @@ specification -> ingestion -> API Semantic IR -> planner -> tool plan
|
|
|
87
87
|
|
|
88
88
|
## Contents
|
|
89
89
|
|
|
90
|
-
- [
|
|
91
|
-
- [
|
|
90
|
+
- [What it does](#what-it-does)
|
|
91
|
+
- [What is not demonstrated](#what-is-not-demonstrated)
|
|
92
|
+
- [Install](#install)
|
|
92
93
|
- [Try it](#try-it)
|
|
93
94
|
- [How it works](#how-it-works)
|
|
94
95
|
- [Design properties](#design-properties)
|
|
@@ -98,22 +99,32 @@ specification -> ingestion -> API Semantic IR -> planner -> tool plan
|
|
|
98
99
|
- [Author](#author)
|
|
99
100
|
- [Licence](#licence)
|
|
100
101
|
|
|
101
|
-
##
|
|
102
|
+
## What it does
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
Ingests **OpenAPI 3.x, Swagger 2.0 and WSDL 1.1**, normalizes them into one provider-independent
|
|
105
|
+
intermediate representation, plans a tool surface, derives a policy manifest, and emits a
|
|
106
|
+
runnable MCP server for the approved part: over HTTP for OpenAPI, over SOAP envelopes for WSDL.
|
|
107
|
+
The compiler itself binds to no MCP SDK, never calls a model, and never reaches the network.
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
transport-independent surface that runs against a deterministic mock. An evaluation harness
|
|
108
|
-
scores a surface against a task corpus using deterministic oracles over real service state,
|
|
109
|
-
driven either by a replay of a recorded solution or by a model that sees only the goal and the
|
|
110
|
-
tools. It emits a runnable MCP server for the approved part of a surface, over HTTP for
|
|
111
|
-
OpenAPI and over SOAP envelopes for WSDL, while the compiler itself binds to no MCP SDK, never
|
|
112
|
-
calls a model, and never reaches the network.
|
|
109
|
+
This has been exercised on specifications and services written by other people, which is where
|
|
110
|
+
the interesting defects live:
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
| Exercised against | Result |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Spotify Web API, 40 operations (RestBench) | Parsed with no blocking ambiguities; least-privilege scopes derived from a real OAuth2 surface |
|
|
115
|
+
| Swagger 2.0 Petstore, 20 operations | Parsed, 13 tools executable, runnable server emitted |
|
|
116
|
+
| 40 WSDL documents (SAWSDL collection) | Parsed, with what cannot be translated recorded rather than approximated |
|
|
117
|
+
| Two live public SOAP services | Real calls made and answered through generated servers |
|
|
118
|
+
|
|
119
|
+
The governance is the part worth having. Every field carries provenance, judgement is proposed
|
|
120
|
+
rather than applied, write and destructive tools are not emitted in executable form until a
|
|
121
|
+
human approves them by class, and what the compiler cannot demonstrate it records as
|
|
122
|
+
unresolved instead of assuming.
|
|
123
|
+
|
|
124
|
+
## What is not demonstrated
|
|
125
|
+
|
|
126
|
+
The design thesis, that a semantically planned surface beats one tool per operation on task
|
|
127
|
+
success, unsafe-action rate and cost, **has been measured four times and is still unanswered**.
|
|
117
128
|
|
|
118
129
|
Every comparison was pre-registered: the hypothesis, corpus, arms, model, success definition
|
|
119
130
|
and significance threshold were fixed in a digested document before the run, and each run
|
|
@@ -121,17 +132,18 @@ records that digest. All returned inconclusive, and the nominal direction revers
|
|
|
121
132
|
them. Nothing here should be read as evidence that the semantic surface performs better. It is
|
|
122
133
|
an argued position that has so far survived no test capable of confirming it.
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
The most distinctive claim, that operations should be composed into workflow tools, is
|
|
136
|
+
untested rather than unsupported. The rule that proposes composites fires nowhere on the first
|
|
137
|
+
benchmark API, and designing a better one after reading that benchmark's solution paths would
|
|
138
|
+
fit the treatment to the test set. It needs a benchmark whose tasks have not been read here.
|
|
139
|
+
|
|
140
|
+
What the runs did establish is mostly about the instrument, and it was worth the tokens.
|
|
141
|
+
Several defects were found only by pointing the system at a specification and a task set
|
|
142
|
+
written by other people: a parser that marked every optional argument required, generated
|
|
143
|
+
schemas no client would load, a store in which a read mutated state and a bulk delete removed
|
|
144
|
+
everything, and oracles that scored the route an annotator took rather than the outcome a goal
|
|
145
|
+
asked for. Each is fixed, and the rules that would have prevented the last class now fail the
|
|
146
|
+
build.
|
|
135
147
|
|
|
136
148
|
## Install
|
|
137
149
|
|
|
@@ -43,8 +43,9 @@ specification -> ingestion -> API Semantic IR -> planner -> tool plan
|
|
|
43
43
|
|
|
44
44
|
## Contents
|
|
45
45
|
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
46
|
+
- [What it does](#what-it-does)
|
|
47
|
+
- [What is not demonstrated](#what-is-not-demonstrated)
|
|
48
|
+
- [Install](#install)
|
|
48
49
|
- [Try it](#try-it)
|
|
49
50
|
- [How it works](#how-it-works)
|
|
50
51
|
- [Design properties](#design-properties)
|
|
@@ -54,22 +55,32 @@ specification -> ingestion -> API Semantic IR -> planner -> tool plan
|
|
|
54
55
|
- [Author](#author)
|
|
55
56
|
- [Licence](#licence)
|
|
56
57
|
|
|
57
|
-
##
|
|
58
|
+
## What it does
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Ingests **OpenAPI 3.x, Swagger 2.0 and WSDL 1.1**, normalizes them into one provider-independent
|
|
61
|
+
intermediate representation, plans a tool surface, derives a policy manifest, and emits a
|
|
62
|
+
runnable MCP server for the approved part: over HTTP for OpenAPI, over SOAP envelopes for WSDL.
|
|
63
|
+
The compiler itself binds to no MCP SDK, never calls a model, and never reaches the network.
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
transport-independent surface that runs against a deterministic mock. An evaluation harness
|
|
64
|
-
scores a surface against a task corpus using deterministic oracles over real service state,
|
|
65
|
-
driven either by a replay of a recorded solution or by a model that sees only the goal and the
|
|
66
|
-
tools. It emits a runnable MCP server for the approved part of a surface, over HTTP for
|
|
67
|
-
OpenAPI and over SOAP envelopes for WSDL, while the compiler itself binds to no MCP SDK, never
|
|
68
|
-
calls a model, and never reaches the network.
|
|
65
|
+
This has been exercised on specifications and services written by other people, which is where
|
|
66
|
+
the interesting defects live:
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
| Exercised against | Result |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Spotify Web API, 40 operations (RestBench) | Parsed with no blocking ambiguities; least-privilege scopes derived from a real OAuth2 surface |
|
|
71
|
+
| Swagger 2.0 Petstore, 20 operations | Parsed, 13 tools executable, runnable server emitted |
|
|
72
|
+
| 40 WSDL documents (SAWSDL collection) | Parsed, with what cannot be translated recorded rather than approximated |
|
|
73
|
+
| Two live public SOAP services | Real calls made and answered through generated servers |
|
|
74
|
+
|
|
75
|
+
The governance is the part worth having. Every field carries provenance, judgement is proposed
|
|
76
|
+
rather than applied, write and destructive tools are not emitted in executable form until a
|
|
77
|
+
human approves them by class, and what the compiler cannot demonstrate it records as
|
|
78
|
+
unresolved instead of assuming.
|
|
79
|
+
|
|
80
|
+
## What is not demonstrated
|
|
81
|
+
|
|
82
|
+
The design thesis, that a semantically planned surface beats one tool per operation on task
|
|
83
|
+
success, unsafe-action rate and cost, **has been measured four times and is still unanswered**.
|
|
73
84
|
|
|
74
85
|
Every comparison was pre-registered: the hypothesis, corpus, arms, model, success definition
|
|
75
86
|
and significance threshold were fixed in a digested document before the run, and each run
|
|
@@ -77,17 +88,18 @@ records that digest. All returned inconclusive, and the nominal direction revers
|
|
|
77
88
|
them. Nothing here should be read as evidence that the semantic surface performs better. It is
|
|
78
89
|
an argued position that has so far survived no test capable of confirming it.
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
The most distinctive claim, that operations should be composed into workflow tools, is
|
|
92
|
+
untested rather than unsupported. The rule that proposes composites fires nowhere on the first
|
|
93
|
+
benchmark API, and designing a better one after reading that benchmark's solution paths would
|
|
94
|
+
fit the treatment to the test set. It needs a benchmark whose tasks have not been read here.
|
|
95
|
+
|
|
96
|
+
What the runs did establish is mostly about the instrument, and it was worth the tokens.
|
|
97
|
+
Several defects were found only by pointing the system at a specification and a task set
|
|
98
|
+
written by other people: a parser that marked every optional argument required, generated
|
|
99
|
+
schemas no client would load, a store in which a read mutated state and a bulk delete removed
|
|
100
|
+
everything, and oracles that scored the route an annotator took rather than the outcome a goal
|
|
101
|
+
asked for. Each is fixed, and the rules that would have prevented the last class now fail the
|
|
102
|
+
build.
|
|
91
103
|
|
|
92
104
|
## Install
|
|
93
105
|
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
name = "api-mcp-compiler"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
license-files = ["LICENSE", "NOTICE"]
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.3.0"
|
|
10
10
|
description = "Semantic and safe compilation of enterprise APIs into MCP tool surfaces"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [{name = "Subhadip Mitra", email = "contact@subhadipmitra.com"}]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/mcp_server.py
RENAMED
|
@@ -33,6 +33,7 @@ from api_mcp_compiler.models import (
|
|
|
33
33
|
EmissionStatus,
|
|
34
34
|
ParameterLocation,
|
|
35
35
|
PolicyManifest,
|
|
36
|
+
RetryPolicy,
|
|
36
37
|
RiskClass,
|
|
37
38
|
ToolDescriptor,
|
|
38
39
|
ToolSurface,
|
|
@@ -108,6 +109,10 @@ def _tool_function(
|
|
|
108
109
|
}
|
|
109
110
|
policy = manifest.policy_for(tool.tool_id) if manifest else None
|
|
110
111
|
confirm = policy is not None and policy.confirmation is not None
|
|
112
|
+
# Derived governance the server has to act on, not merely carry: a retry policy nothing
|
|
113
|
+
# reads is the same defect as a confirmation nobody expires.
|
|
114
|
+
retry = policy.retry.value if policy else RetryPolicy.NEVER.value
|
|
115
|
+
needs_key = policy.idempotency_key_required if policy else False
|
|
111
116
|
confirmation_ttl = (
|
|
112
117
|
policy.confirmation.token_ttl_seconds
|
|
113
118
|
if policy is not None and policy.confirmation is not None
|
|
@@ -130,6 +135,8 @@ async def {tool.name}(arguments: dict[str, Any]) -> dict[str, Any]:
|
|
|
130
135
|
bindings={_binding_map(tool)!r},
|
|
131
136
|
requires_confirmation={confirm!r},
|
|
132
137
|
confirmation_ttl_seconds={confirmation_ttl!r},
|
|
138
|
+
retry={retry!r},
|
|
139
|
+
idempotency_key_required={needs_key!r},
|
|
133
140
|
destructive={destructive!r},
|
|
134
141
|
max_output_bytes={max_bytes!r},
|
|
135
142
|
redact_fields={redact!r},
|
|
@@ -152,9 +159,11 @@ registered them would undo the decision at the last step.
|
|
|
152
159
|
|
|
153
160
|
from __future__ import annotations
|
|
154
161
|
|
|
162
|
+
import asyncio
|
|
155
163
|
import json
|
|
156
164
|
import os
|
|
157
165
|
import time
|
|
166
|
+
import uuid
|
|
158
167
|
from typing import Any
|
|
159
168
|
|
|
160
169
|
import httpx
|
|
@@ -185,6 +194,73 @@ def _digest(tool_name: str, arguments: dict[str, Any]) -> str:
|
|
|
185
194
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
186
195
|
|
|
187
196
|
|
|
197
|
+
#: Attempts in total, not retries in addition. Three is a compromise: enough to ride out a
|
|
198
|
+
#: single restart or rate-limit window, few enough that a wedged upstream is reported rather
|
|
199
|
+
#: than hammered.
|
|
200
|
+
_MAX_ATTEMPTS = 3
|
|
201
|
+
|
|
202
|
+
#: Status codes worth sending again. Deliberately excludes 500: a server error may mean the
|
|
203
|
+
#: effect happened and the response was lost, and repeating that is exactly what the retry
|
|
204
|
+
#: policy exists to prevent. 429 and the gateway codes are unambiguous about not having acted.
|
|
205
|
+
_RETRYABLE_STATUS = frozenset({{429, 502, 503, 504}})
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _backoff(attempt: int, response: Any) -> float:
|
|
209
|
+
"""How long to wait, preferring what the service asked for over what we guessed."""
|
|
210
|
+
if response is not None:
|
|
211
|
+
requested = getattr(response, "headers", {{}}) or {{}}
|
|
212
|
+
raw = requested.get("Retry-After") or requested.get("retry-after")
|
|
213
|
+
if raw:
|
|
214
|
+
try:
|
|
215
|
+
return max(0.0, float(raw))
|
|
216
|
+
except (TypeError, ValueError):
|
|
217
|
+
pass
|
|
218
|
+
return float(2**attempt) / 2
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
async def _send(
|
|
222
|
+
client: Any,
|
|
223
|
+
method: str,
|
|
224
|
+
path: str,
|
|
225
|
+
*,
|
|
226
|
+
params: Any,
|
|
227
|
+
headers: dict[str, str],
|
|
228
|
+
json_body: Any,
|
|
229
|
+
retry: str,
|
|
230
|
+
idempotency_key: str | None,
|
|
231
|
+
) -> Any:
|
|
232
|
+
"""Make one request, repeating it only as far as the derived policy allows.
|
|
233
|
+
|
|
234
|
+
`never` means one attempt. It is the policy for an operation whose idempotency could not
|
|
235
|
+
be determined, where a repeat could duplicate an effect nobody asked for twice.
|
|
236
|
+
|
|
237
|
+
An idempotency key is generated per invocation and held across that invocation's retries,
|
|
238
|
+
which is the whole point: a fresh key per attempt would make every retry a new operation,
|
|
239
|
+
and a key derived from the arguments would make two deliberate identical calls collide.
|
|
240
|
+
"""
|
|
241
|
+
attempts = 1 if retry == "never" else _MAX_ATTEMPTS
|
|
242
|
+
for attempt in range(attempts):
|
|
243
|
+
sending = dict(headers)
|
|
244
|
+
if idempotency_key is not None:
|
|
245
|
+
sending["Idempotency-Key"] = idempotency_key
|
|
246
|
+
last_error: Exception | None = None
|
|
247
|
+
try:
|
|
248
|
+
response = await client.request(
|
|
249
|
+
method, path, params=params, headers=sending or None, json=json_body
|
|
250
|
+
)
|
|
251
|
+
except Exception as error: # transport failure: nothing was answered, so it is safe
|
|
252
|
+
last_error = error
|
|
253
|
+
if attempt + 1 >= attempts:
|
|
254
|
+
raise
|
|
255
|
+
await asyncio.sleep(_backoff(attempt, None))
|
|
256
|
+
continue
|
|
257
|
+
if response.status_code in _RETRYABLE_STATUS and attempt + 1 < attempts:
|
|
258
|
+
await asyncio.sleep(_backoff(attempt, response))
|
|
259
|
+
continue
|
|
260
|
+
return response
|
|
261
|
+
raise last_error if last_error is not None else RuntimeError("no attempt was made")
|
|
262
|
+
|
|
263
|
+
|
|
188
264
|
def _credentials(tool_name: str) -> tuple[dict[str, str], dict[str, str]]:
|
|
189
265
|
"""Read the credentials this tool needs and place each where the service declared it.
|
|
190
266
|
|
|
@@ -263,6 +339,8 @@ async def _invoke(
|
|
|
263
339
|
bindings: dict[str, tuple[str, str, str | None]],
|
|
264
340
|
requires_confirmation: bool,
|
|
265
341
|
confirmation_ttl_seconds: int,
|
|
342
|
+
retry: str,
|
|
343
|
+
idempotency_key_required: bool,
|
|
266
344
|
destructive: bool,
|
|
267
345
|
max_output_bytes: int | None,
|
|
268
346
|
redact_fields: list[str],
|
|
@@ -316,6 +394,7 @@ async def _invoke(
|
|
|
316
394
|
carried = dict(arguments)
|
|
317
395
|
responses: list[Any] = []
|
|
318
396
|
status_code = 0
|
|
397
|
+
idempotency_key = str(uuid.uuid4()) if idempotency_key_required else None
|
|
319
398
|
|
|
320
399
|
async with httpx.AsyncClient(base_url=BASE_URL, timeout=30) as client:
|
|
321
400
|
for position, (method, route, operation_id) in enumerate(steps):
|
|
@@ -352,8 +431,15 @@ async def _invoke(
|
|
|
352
431
|
headers.update(authorization)
|
|
353
432
|
query.update(authorized_query)
|
|
354
433
|
|
|
355
|
-
response = await
|
|
356
|
-
|
|
434
|
+
response = await _send(
|
|
435
|
+
client,
|
|
436
|
+
method,
|
|
437
|
+
path,
|
|
438
|
+
params=query or None,
|
|
439
|
+
headers=headers,
|
|
440
|
+
json_body=body,
|
|
441
|
+
retry=retry,
|
|
442
|
+
idempotency_key=idempotency_key,
|
|
357
443
|
)
|
|
358
444
|
status_code = response.status_code
|
|
359
445
|
try:
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/soap_server.py
RENAMED
|
@@ -23,6 +23,7 @@ from api_mcp_compiler.models import (
|
|
|
23
23
|
OperationIR,
|
|
24
24
|
ParameterLocation,
|
|
25
25
|
PolicyManifest,
|
|
26
|
+
RetryPolicy,
|
|
26
27
|
RiskClass,
|
|
27
28
|
ToolDescriptor,
|
|
28
29
|
ToolSurface,
|
|
@@ -65,6 +66,7 @@ def _tool_function(ir: ApiSemanticIR, tool: ToolDescriptor, manifest: PolicyMani
|
|
|
65
66
|
raise SoapEmissionError(f"{tool.name!r} carries no SOAP binding record")
|
|
66
67
|
policy = manifest.policy_for(tool.tool_id) if manifest else None
|
|
67
68
|
confirm = policy is not None and policy.confirmation is not None
|
|
69
|
+
retry = policy.retry.value if policy else RetryPolicy.NEVER.value
|
|
68
70
|
confirmation_ttl = (
|
|
69
71
|
policy.confirmation.token_ttl_seconds
|
|
70
72
|
if policy is not None and policy.confirmation is not None
|
|
@@ -96,6 +98,7 @@ async def {tool.name}(arguments: dict[str, Any]) -> dict[str, Any]:
|
|
|
96
98
|
schema=_SCHEMAS[{tool.name!r}],
|
|
97
99
|
requires_confirmation={confirm!r},
|
|
98
100
|
confirmation_ttl_seconds={confirmation_ttl!r},
|
|
101
|
+
retry={retry!r},
|
|
99
102
|
destructive={(tool.risk is RiskClass.DESTRUCTIVE)!r},
|
|
100
103
|
max_output_bytes={(policy.output.max_bytes if policy else None)!r},
|
|
101
104
|
redact_fields={sorted(policy.output.redact_fields) if policy else []!r},
|
|
@@ -117,6 +120,7 @@ produces a request the service rejects while looking like it worked.
|
|
|
117
120
|
|
|
118
121
|
from __future__ import annotations
|
|
119
122
|
|
|
123
|
+
import asyncio
|
|
120
124
|
import json
|
|
121
125
|
import os
|
|
122
126
|
import time
|
|
@@ -153,6 +157,51 @@ def _digest(tool_name: str, arguments: dict[str, Any]) -> str:
|
|
|
153
157
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
154
158
|
|
|
155
159
|
|
|
160
|
+
#: Attempts in total, not retries in addition.
|
|
161
|
+
_MAX_ATTEMPTS = 3
|
|
162
|
+
|
|
163
|
+
#: Transport-level codes worth sending again. A SOAP fault arrives as 500 and is an answer,
|
|
164
|
+
#: not a failure to answer, so 500 is never retried here.
|
|
165
|
+
_RETRYABLE_STATUS = frozenset({{429, 502, 503, 504}})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _backoff(attempt: int, response: Any) -> float:
|
|
169
|
+
"""How long to wait, preferring what the service asked for over what we guessed."""
|
|
170
|
+
if response is not None:
|
|
171
|
+
requested = getattr(response, "headers", {{}}) or {{}}
|
|
172
|
+
raw = requested.get("Retry-After") or requested.get("retry-after")
|
|
173
|
+
if raw:
|
|
174
|
+
try:
|
|
175
|
+
return max(0.0, float(raw))
|
|
176
|
+
except (TypeError, ValueError):
|
|
177
|
+
pass
|
|
178
|
+
return float(2**attempt) / 2
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def _post(client: Any, envelope: str, headers: dict[str, str], query: Any, retry: str) -> Any:
|
|
182
|
+
"""Post the envelope, repeating it only as far as the derived policy allows.
|
|
183
|
+
|
|
184
|
+
No idempotency key is sent. WSDL declares nothing equivalent, so a key here would be a
|
|
185
|
+
header invented by this compiler that no service was built to honour, and a retry of a
|
|
186
|
+
non-idempotent SOAP operation is exactly what `never` is for.
|
|
187
|
+
"""
|
|
188
|
+
for attempt in range(1 if retry == "never" else _MAX_ATTEMPTS):
|
|
189
|
+
try:
|
|
190
|
+
response = await client.post(
|
|
191
|
+
ENDPOINT, content=envelope.encode("utf-8"), headers=headers, params=query or None
|
|
192
|
+
)
|
|
193
|
+
except Exception: # nothing was answered, so nothing was performed twice
|
|
194
|
+
if attempt + 1 >= (1 if retry == "never" else _MAX_ATTEMPTS):
|
|
195
|
+
raise
|
|
196
|
+
await asyncio.sleep(_backoff(attempt, None))
|
|
197
|
+
continue
|
|
198
|
+
if response.status_code in _RETRYABLE_STATUS and attempt + 1 < _MAX_ATTEMPTS:
|
|
199
|
+
await asyncio.sleep(_backoff(attempt, response))
|
|
200
|
+
continue
|
|
201
|
+
return response
|
|
202
|
+
raise RuntimeError("no attempt was made")
|
|
203
|
+
|
|
204
|
+
|
|
156
205
|
def _credentials(tool_name: str) -> tuple[dict[str, str], dict[str, str]]:
|
|
157
206
|
"""Read the credentials this tool needs and place each where the service declared it.
|
|
158
207
|
|
|
@@ -252,6 +301,7 @@ async def _invoke(
|
|
|
252
301
|
schema: dict[str, Any],
|
|
253
302
|
requires_confirmation: bool,
|
|
254
303
|
confirmation_ttl_seconds: int,
|
|
304
|
+
retry: str,
|
|
255
305
|
destructive: bool,
|
|
256
306
|
max_output_bytes: int | None,
|
|
257
307
|
redact_fields: list[str],
|
|
@@ -330,12 +380,7 @@ async def _invoke(
|
|
|
330
380
|
headers.update(authorization)
|
|
331
381
|
|
|
332
382
|
async with httpx.AsyncClient(timeout=30) as client:
|
|
333
|
-
response = await client
|
|
334
|
-
ENDPOINT,
|
|
335
|
-
content=envelope.encode("utf-8"),
|
|
336
|
-
headers=headers,
|
|
337
|
-
params=query or None,
|
|
338
|
-
)
|
|
383
|
+
response = await _post(client, envelope, headers, query, retry)
|
|
339
384
|
|
|
340
385
|
try:
|
|
341
386
|
root = ElementTree.fromstring(response.text)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-mcp-compiler
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Semantic and safe compilation of enterprise APIs into MCP tool surfaces
|
|
5
5
|
Author-email: Subhadip Mitra <contact@subhadipmitra.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -87,8 +87,9 @@ specification -> ingestion -> API Semantic IR -> planner -> tool plan
|
|
|
87
87
|
|
|
88
88
|
## Contents
|
|
89
89
|
|
|
90
|
-
- [
|
|
91
|
-
- [
|
|
90
|
+
- [What it does](#what-it-does)
|
|
91
|
+
- [What is not demonstrated](#what-is-not-demonstrated)
|
|
92
|
+
- [Install](#install)
|
|
92
93
|
- [Try it](#try-it)
|
|
93
94
|
- [How it works](#how-it-works)
|
|
94
95
|
- [Design properties](#design-properties)
|
|
@@ -98,22 +99,32 @@ specification -> ingestion -> API Semantic IR -> planner -> tool plan
|
|
|
98
99
|
- [Author](#author)
|
|
99
100
|
- [Licence](#licence)
|
|
100
101
|
|
|
101
|
-
##
|
|
102
|
+
## What it does
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
Ingests **OpenAPI 3.x, Swagger 2.0 and WSDL 1.1**, normalizes them into one provider-independent
|
|
105
|
+
intermediate representation, plans a tool surface, derives a policy manifest, and emits a
|
|
106
|
+
runnable MCP server for the approved part: over HTTP for OpenAPI, over SOAP envelopes for WSDL.
|
|
107
|
+
The compiler itself binds to no MCP SDK, never calls a model, and never reaches the network.
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
transport-independent surface that runs against a deterministic mock. An evaluation harness
|
|
108
|
-
scores a surface against a task corpus using deterministic oracles over real service state,
|
|
109
|
-
driven either by a replay of a recorded solution or by a model that sees only the goal and the
|
|
110
|
-
tools. It emits a runnable MCP server for the approved part of a surface, over HTTP for
|
|
111
|
-
OpenAPI and over SOAP envelopes for WSDL, while the compiler itself binds to no MCP SDK, never
|
|
112
|
-
calls a model, and never reaches the network.
|
|
109
|
+
This has been exercised on specifications and services written by other people, which is where
|
|
110
|
+
the interesting defects live:
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
| Exercised against | Result |
|
|
113
|
+
|---|---|
|
|
114
|
+
| Spotify Web API, 40 operations (RestBench) | Parsed with no blocking ambiguities; least-privilege scopes derived from a real OAuth2 surface |
|
|
115
|
+
| Swagger 2.0 Petstore, 20 operations | Parsed, 13 tools executable, runnable server emitted |
|
|
116
|
+
| 40 WSDL documents (SAWSDL collection) | Parsed, with what cannot be translated recorded rather than approximated |
|
|
117
|
+
| Two live public SOAP services | Real calls made and answered through generated servers |
|
|
118
|
+
|
|
119
|
+
The governance is the part worth having. Every field carries provenance, judgement is proposed
|
|
120
|
+
rather than applied, write and destructive tools are not emitted in executable form until a
|
|
121
|
+
human approves them by class, and what the compiler cannot demonstrate it records as
|
|
122
|
+
unresolved instead of assuming.
|
|
123
|
+
|
|
124
|
+
## What is not demonstrated
|
|
125
|
+
|
|
126
|
+
The design thesis, that a semantically planned surface beats one tool per operation on task
|
|
127
|
+
success, unsafe-action rate and cost, **has been measured four times and is still unanswered**.
|
|
117
128
|
|
|
118
129
|
Every comparison was pre-registered: the hypothesis, corpus, arms, model, success definition
|
|
119
130
|
and significance threshold were fixed in a digested document before the run, and each run
|
|
@@ -121,17 +132,18 @@ records that digest. All returned inconclusive, and the nominal direction revers
|
|
|
121
132
|
them. Nothing here should be read as evidence that the semantic surface performs better. It is
|
|
122
133
|
an argued position that has so far survived no test capable of confirming it.
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
The most distinctive claim, that operations should be composed into workflow tools, is
|
|
136
|
+
untested rather than unsupported. The rule that proposes composites fires nowhere on the first
|
|
137
|
+
benchmark API, and designing a better one after reading that benchmark's solution paths would
|
|
138
|
+
fit the treatment to the test set. It needs a benchmark whose tasks have not been read here.
|
|
139
|
+
|
|
140
|
+
What the runs did establish is mostly about the instrument, and it was worth the tokens.
|
|
141
|
+
Several defects were found only by pointing the system at a specification and a task set
|
|
142
|
+
written by other people: a parser that marked every optional argument required, generated
|
|
143
|
+
schemas no client would load, a store in which a read mutated state and a bulk delete removed
|
|
144
|
+
everything, and oracles that scored the route an annotator took rather than the outcome a goal
|
|
145
|
+
asked for. Each is fixed, and the rules that would have prevented the last class now fail the
|
|
146
|
+
build.
|
|
135
147
|
|
|
136
148
|
## Install
|
|
137
149
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Tests that a generated server retries exactly as far as the derived policy allows.
|
|
2
|
+
|
|
3
|
+
The manifest has always computed a retry policy and an idempotency requirement per tool, and
|
|
4
|
+
the generated server read neither: a `never` policy retried nothing because nothing retried
|
|
5
|
+
at all, which looks like compliance and is absence. This is the third value in a row that was
|
|
6
|
+
derived, written into the artifact, and acted on by nobody, after the confirmation time to
|
|
7
|
+
live and the credential placement.
|
|
8
|
+
|
|
9
|
+
As with those, the tests here run the emitted module and watch what it does to the wire.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import sys
|
|
16
|
+
import types
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from api_mcp_compiler.codegen.mcp_server import emit_server
|
|
23
|
+
from api_mcp_compiler.codegen.tools import generate_surface
|
|
24
|
+
from api_mcp_compiler.ingest.openapi import parse_openapi
|
|
25
|
+
from api_mcp_compiler.models import RiskClass
|
|
26
|
+
from api_mcp_compiler.planning.approval import approve
|
|
27
|
+
from api_mcp_compiler.planning.semantic import plan_semantic
|
|
28
|
+
from api_mcp_compiler.policy.synthesis import synthesize_policy
|
|
29
|
+
|
|
30
|
+
#: `put` is idempotent by RFC 9110 and `post` is not, which is what makes the two tools below
|
|
31
|
+
#: land on different retry policies without anything in the test saying so.
|
|
32
|
+
SERVICE = """openapi: 3.0.3
|
|
33
|
+
info: {title: Retry Service, version: 1.0.0}
|
|
34
|
+
servers: [{url: https://retry.example.invalid}]
|
|
35
|
+
# A write with no declared authentication cannot be shown to be governed, so the emission
|
|
36
|
+
# gate refuses it and there is nothing to test. That refusal is the subject of its own tests.
|
|
37
|
+
security: [{guard: []}]
|
|
38
|
+
paths:
|
|
39
|
+
/widgets/{id}:
|
|
40
|
+
put:
|
|
41
|
+
operationId: replaceWidget
|
|
42
|
+
summary: Replace a widget
|
|
43
|
+
parameters: [{in: path, name: id, required: true, schema: {type: string}}]
|
|
44
|
+
responses: {'200': {description: ok, content: {application/json: {schema: {type: object}}}}}
|
|
45
|
+
/widgets:
|
|
46
|
+
post:
|
|
47
|
+
operationId: createWidget
|
|
48
|
+
summary: Create a widget
|
|
49
|
+
responses: {'201': {description: made, content: {application/json: {schema: {type: object}}}}}
|
|
50
|
+
components:
|
|
51
|
+
securitySchemes:
|
|
52
|
+
guard: {type: apiKey, in: header, name: X-Retry-Key}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _Reply:
|
|
57
|
+
def __init__(self, status: int, headers: dict[str, str] | None = None) -> None:
|
|
58
|
+
self.status_code = status
|
|
59
|
+
self.headers = headers or {}
|
|
60
|
+
|
|
61
|
+
def json(self) -> dict[str, str]:
|
|
62
|
+
return {"id": "w-1"}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def text(self) -> str:
|
|
66
|
+
return "{}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _Client:
|
|
70
|
+
"""Answers with a scripted sequence, recording every attempt."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, script: list[Any], seen: list[dict[str, Any]]) -> None:
|
|
73
|
+
self.script = script
|
|
74
|
+
self.seen = seen
|
|
75
|
+
|
|
76
|
+
async def __aenter__(self) -> _Client:
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
async def __aexit__(self, *_: object) -> None:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
async def request(
|
|
83
|
+
self, method: str, path: str, params: Any = None, headers: Any = None, json: Any = None
|
|
84
|
+
) -> Any:
|
|
85
|
+
self.seen.append({"method": method, "path": path, "headers": dict(headers or {})})
|
|
86
|
+
answer = self.script.pop(0) if self.script else _Reply(200)
|
|
87
|
+
if isinstance(answer, Exception):
|
|
88
|
+
raise answer
|
|
89
|
+
return answer
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _load(
|
|
93
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, script: list[Any]
|
|
94
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]], list[float]]:
|
|
95
|
+
"""Emit the server, stub the SDK and the clock, and return what it did."""
|
|
96
|
+
spec = tmp_path / "service.yaml"
|
|
97
|
+
spec.write_text(SERVICE, encoding="utf-8")
|
|
98
|
+
ir = parse_openapi(spec)
|
|
99
|
+
plan = plan_semantic(ir)
|
|
100
|
+
overlay = approve(plan, overlay=None, risk=RiskClass.WRITE, group=None, names=[]).overlay
|
|
101
|
+
approved = plan_semantic(ir, overlay)
|
|
102
|
+
manifest = synthesize_policy(ir, approved)
|
|
103
|
+
source = emit_server(ir, generate_surface(ir, approved, manifest), manifest).source
|
|
104
|
+
|
|
105
|
+
seen: list[dict[str, Any]] = []
|
|
106
|
+
fastmcp = types.ModuleType("mcp.server.fastmcp")
|
|
107
|
+
fastmcp.FastMCP = lambda *_, **__: types.SimpleNamespace( # type: ignore[attr-defined]
|
|
108
|
+
tool=lambda **_kw: (lambda function: function),
|
|
109
|
+
resource=lambda *_a, **_kw: (lambda function: function),
|
|
110
|
+
)
|
|
111
|
+
server = types.ModuleType("mcp.server")
|
|
112
|
+
server.fastmcp = fastmcp # type: ignore[attr-defined]
|
|
113
|
+
package = types.ModuleType("mcp")
|
|
114
|
+
package.server = server # type: ignore[attr-defined]
|
|
115
|
+
httpx = types.ModuleType("httpx")
|
|
116
|
+
httpx.AsyncClient = lambda **_: _Client(script, seen) # type: ignore[attr-defined]
|
|
117
|
+
for name, module in {
|
|
118
|
+
"mcp": package, "mcp.server": server, "mcp.server.fastmcp": fastmcp, "httpx": httpx
|
|
119
|
+
}.items():
|
|
120
|
+
monkeypatch.setitem(sys.modules, name, module)
|
|
121
|
+
|
|
122
|
+
namespace: dict[str, Any] = {"__name__": "generated_server"}
|
|
123
|
+
exec(compile(source, "<generated server>", "exec"), namespace)
|
|
124
|
+
|
|
125
|
+
# Waiting is the one part that must not actually happen in a test, but how long it would
|
|
126
|
+
# have waited is worth asserting, so it is recorded rather than discarded.
|
|
127
|
+
waits: list[float] = []
|
|
128
|
+
|
|
129
|
+
async def _instant(delay: float) -> None:
|
|
130
|
+
waits.append(delay)
|
|
131
|
+
|
|
132
|
+
namespace["asyncio"] = types.SimpleNamespace(sleep=_instant)
|
|
133
|
+
return namespace, seen, waits
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _replace(namespace: dict[str, Any]) -> Any:
|
|
137
|
+
"""The idempotent tool, whose derived policy is `safe`."""
|
|
138
|
+
return asyncio.run(namespace["replace_widget"]({"id": "w-1"}))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _create(namespace: dict[str, Any]) -> Any:
|
|
142
|
+
"""The non-idempotent tool, whose derived policy needs an idempotency key."""
|
|
143
|
+
return asyncio.run(namespace["create_widget"]({}))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_a_retryable_status_is_sent_again(
|
|
147
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
148
|
+
) -> None:
|
|
149
|
+
namespace, seen, waits = _load(tmp_path, monkeypatch, [_Reply(503), _Reply(200)])
|
|
150
|
+
|
|
151
|
+
_replace(namespace)
|
|
152
|
+
|
|
153
|
+
assert len(seen) == 2
|
|
154
|
+
assert waits == [0.5]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_a_client_error_is_not_sent_again(
|
|
158
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Repeating a request the service rejected changes nothing except the load."""
|
|
161
|
+
namespace, seen, _ = _load(tmp_path, monkeypatch, [_Reply(404), _Reply(200)])
|
|
162
|
+
|
|
163
|
+
_replace(namespace)
|
|
164
|
+
|
|
165
|
+
assert len(seen) == 1
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_a_server_error_is_not_sent_again(
|
|
169
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
170
|
+
) -> None:
|
|
171
|
+
"""500 may mean the effect happened and the answer was lost, which a retry duplicates."""
|
|
172
|
+
namespace, seen, _ = _load(tmp_path, monkeypatch, [_Reply(500), _Reply(200)])
|
|
173
|
+
|
|
174
|
+
_replace(namespace)
|
|
175
|
+
|
|
176
|
+
assert len(seen) == 1
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_attempts_are_bounded(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
180
|
+
"""A wedged upstream is reported, not hammered."""
|
|
181
|
+
namespace, seen, waits = _load(
|
|
182
|
+
tmp_path, monkeypatch, [_Reply(503), _Reply(503), _Reply(503), _Reply(200)]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
_replace(namespace)
|
|
186
|
+
|
|
187
|
+
assert len(seen) == 3
|
|
188
|
+
assert waits == [0.5, 1.0]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_the_service_decides_how_long_to_wait(
|
|
192
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
193
|
+
) -> None:
|
|
194
|
+
"""A rate limit that names its window is more useful than any backoff curve."""
|
|
195
|
+
namespace, _, waits = _load(
|
|
196
|
+
tmp_path, monkeypatch, [_Reply(429, {"Retry-After": "7"}), _Reply(200)]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
_replace(namespace)
|
|
200
|
+
|
|
201
|
+
assert waits == [7.0]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_a_transport_failure_is_retried(
|
|
205
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Nothing was answered, so nothing was performed twice."""
|
|
208
|
+
namespace, seen, _ = _load(
|
|
209
|
+
tmp_path, monkeypatch, [ConnectionError("reset"), _Reply(200)]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
_replace(namespace)
|
|
213
|
+
|
|
214
|
+
assert len(seen) == 2
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_a_non_idempotent_tool_carries_a_key_that_survives_the_retry(
|
|
218
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
219
|
+
) -> None:
|
|
220
|
+
"""A fresh key per attempt would make every retry a new operation."""
|
|
221
|
+
namespace, seen, _ = _load(tmp_path, monkeypatch, [_Reply(503), _Reply(201)])
|
|
222
|
+
|
|
223
|
+
_create(namespace)
|
|
224
|
+
|
|
225
|
+
keys = [attempt["headers"].get("Idempotency-Key") for attempt in seen]
|
|
226
|
+
assert len(seen) == 2
|
|
227
|
+
assert keys[0] and keys[0] == keys[1]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_two_invocations_do_not_share_a_key(
|
|
231
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
232
|
+
) -> None:
|
|
233
|
+
"""A key derived from the arguments would make two deliberate identical calls collide."""
|
|
234
|
+
namespace, seen, _ = _load(tmp_path, monkeypatch, [_Reply(201), _Reply(201)])
|
|
235
|
+
|
|
236
|
+
_create(namespace)
|
|
237
|
+
_create(namespace)
|
|
238
|
+
|
|
239
|
+
first, second = (attempt["headers"]["Idempotency-Key"] for attempt in seen)
|
|
240
|
+
assert first != second
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_an_idempotent_tool_sends_no_key(
|
|
244
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
245
|
+
) -> None:
|
|
246
|
+
"""The header is for the case that needs it, not decoration on every request."""
|
|
247
|
+
namespace, seen, _ = _load(tmp_path, monkeypatch, [_Reply(200)])
|
|
248
|
+
|
|
249
|
+
_replace(namespace)
|
|
250
|
+
|
|
251
|
+
assert "Idempotency-Key" not in seen[0]["headers"]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_invalid_arguments_reach_no_request_at_all(
|
|
255
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Validating after calling has already had the effect it was checking for."""
|
|
258
|
+
namespace, seen, _ = _load(tmp_path, monkeypatch, [_Reply(200)])
|
|
259
|
+
|
|
260
|
+
outcome = asyncio.run(namespace["replace_widget"]({"id": 17}))
|
|
261
|
+
|
|
262
|
+
assert outcome["error"] == "invalid_arguments"
|
|
263
|
+
assert seen == []
|
|
@@ -94,11 +94,16 @@ def test_no_credential_is_written_into_the_generated_file() -> None:
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def test_arguments_are_validated_before_the_call_is_made() -> None:
|
|
97
|
-
"""A surface that validates after calling has already had the effect it was checking.
|
|
97
|
+
"""A surface that validates after calling has already had the effect it was checking.
|
|
98
|
+
|
|
99
|
+
Asserted on the ordering inside `_invoke`, which is where the decision lives. The request
|
|
100
|
+
itself is made from a helper defined further up the file, so comparing raw positions
|
|
101
|
+
would compare the wrong two things. `test_generated_retry.py` checks the same property
|
|
102
|
+
by running the module and finding no request was made at all.
|
|
103
|
+
"""
|
|
98
104
|
_, _, emitted = _emit()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert validate_at < call_at
|
|
105
|
+
body = emitted.source[emitted.source.index("async def _invoke("):]
|
|
106
|
+
assert body.index("iter_errors(arguments)") < body.index("await _send(")
|
|
102
107
|
|
|
103
108
|
|
|
104
109
|
def test_a_surface_with_nothing_executable_refuses_to_emit() -> None:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/codegen/credentials.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/__init__.py
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/harness.py
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/model_driver.py
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/oracles.py
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/preregistration.py
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/evaluation/restbench.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/reporting/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/runtime/governance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler/schemas/tool_plan.schema.json
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/requires.txt
RENAMED
|
File without changes
|
{api_mcp_compiler-0.2.0 → api_mcp_compiler-0.3.0}/src/api_mcp_compiler.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|