nocfo-cli 1.3.0__tar.gz → 1.4.1__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.
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/PKG-INFO +1 -1
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/pyproject.toml +1 -1
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/contract_validation.py +4 -2
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/http_error_capture.py +18 -8
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/server.py +28 -1
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/LICENSE +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/README.md +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/__init__.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/api_client.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/__init__.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/app.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/files.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/products.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/schema.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/user.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/context.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/output.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/config.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/__init__.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/auth.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/middleware.py +0 -0
- {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/openapi.py +0 -0
|
@@ -13,6 +13,7 @@ from nocfo_toolkit.mcp.server import (
|
|
|
13
13
|
MCP_OPENAPI_ROUTE_MAPS,
|
|
14
14
|
apply_mcp_operation_metadata,
|
|
15
15
|
apply_mcp_namespace_names,
|
|
16
|
+
restore_openapi_output_schema,
|
|
16
17
|
)
|
|
17
18
|
from nocfo_toolkit.openapi import filter_mcp_spec
|
|
18
19
|
|
|
@@ -35,6 +36,7 @@ class MCPContractValidationResult:
|
|
|
35
36
|
def _apply_component_mcp_metadata(route: Any, component: Any) -> None:
|
|
36
37
|
apply_mcp_namespace_names(route, component)
|
|
37
38
|
apply_mcp_operation_metadata(route, component)
|
|
39
|
+
restore_openapi_output_schema(route, component)
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
def _get_base_url(openapi_spec: dict[str, Any]) -> str:
|
|
@@ -146,7 +148,7 @@ def _build_result(
|
|
|
146
148
|
def validate_openapi_mcp_contract(
|
|
147
149
|
openapi_spec: dict[str, Any],
|
|
148
150
|
*,
|
|
149
|
-
validate_output: bool =
|
|
151
|
+
validate_output: bool = False,
|
|
150
152
|
) -> MCPContractValidationResult:
|
|
151
153
|
"""Validate that MCP-tagged operations map cleanly to FastMCP components.
|
|
152
154
|
|
|
@@ -186,7 +188,7 @@ def validate_openapi_mcp_contract(
|
|
|
186
188
|
def assert_openapi_mcp_contract_valid(
|
|
187
189
|
openapi_spec: dict[str, Any],
|
|
188
190
|
*,
|
|
189
|
-
validate_output: bool =
|
|
191
|
+
validate_output: bool = False,
|
|
190
192
|
) -> MCPContractValidationResult:
|
|
191
193
|
"""Assert MCP contract validity and return the validation result."""
|
|
192
194
|
result = validate_openapi_mcp_contract(
|
|
@@ -21,21 +21,31 @@ def get_last_http_error() -> dict[str, Any] | None:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
async def capture_http_error_response(response: httpx.Response) -> None:
|
|
24
|
-
"""Store status/payload for the latest
|
|
24
|
+
"""Store status/payload for the latest HTTP response.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
FastMCP can issue downstream requests with streaming enabled. In that mode,
|
|
27
|
+
accessing ``response.content``/``response.json`` without ``aread()`` raises
|
|
28
|
+
``ResponseNotRead``. We therefore always drain the body first, even for
|
|
29
|
+
success responses, so tool handlers can safely decode the payload.
|
|
30
|
+
"""
|
|
29
31
|
|
|
30
32
|
try:
|
|
31
33
|
await response.aread()
|
|
32
|
-
except httpx.HTTPError:
|
|
33
|
-
|
|
34
|
+
except (httpx.HTTPError, httpx.StreamError):
|
|
35
|
+
# Keep best-effort behavior: failures here should not shadow the original
|
|
36
|
+
# tool error path. The middleware still clears stale captured state below.
|
|
37
|
+
_LAST_HTTP_ERROR.set(None)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
if response.status_code < 400:
|
|
41
|
+
_LAST_HTTP_ERROR.set(None)
|
|
42
|
+
return
|
|
34
43
|
|
|
35
44
|
try:
|
|
36
45
|
payload: Any = response.json() if response.content else None
|
|
37
|
-
except
|
|
38
|
-
|
|
46
|
+
except ValueError:
|
|
47
|
+
text = response.text.strip()
|
|
48
|
+
payload = text[:1000] if text else None
|
|
39
49
|
|
|
40
50
|
_LAST_HTTP_ERROR.set(
|
|
41
51
|
{
|
|
@@ -32,6 +32,8 @@ from nocfo_toolkit.openapi import (
|
|
|
32
32
|
load_openapi_spec,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
+
from fastmcp.utilities.openapi import extract_output_schema_from_responses
|
|
36
|
+
|
|
35
37
|
if TYPE_CHECKING:
|
|
36
38
|
from fastmcp import FastMCP
|
|
37
39
|
|
|
@@ -133,6 +135,29 @@ def apply_mcp_operation_metadata(route: Any, component: Any) -> None:
|
|
|
133
135
|
component.meta = meta
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
def restore_openapi_output_schema(route: Any, component: Any) -> None:
|
|
139
|
+
"""Restore the real OpenAPI output schema on tools for agent-facing metadata.
|
|
140
|
+
|
|
141
|
+
When ``validate_output=False`` FastMCP replaces the output schema with a
|
|
142
|
+
permissive fallback so that runtime responses are never rejected. This
|
|
143
|
+
function re-derives the accurate schema from the route and writes it back
|
|
144
|
+
onto the tool so that ``tools/list`` still exposes precise type information
|
|
145
|
+
to the agent — without enforcing it at call time.
|
|
146
|
+
"""
|
|
147
|
+
if not isinstance(component, Tool):
|
|
148
|
+
return
|
|
149
|
+
responses = getattr(route, "responses", None)
|
|
150
|
+
if responses is None:
|
|
151
|
+
return
|
|
152
|
+
real_schema = extract_output_schema_from_responses(
|
|
153
|
+
responses,
|
|
154
|
+
getattr(route, "response_schemas", None),
|
|
155
|
+
getattr(route, "openapi_version", None),
|
|
156
|
+
)
|
|
157
|
+
if real_schema is not None:
|
|
158
|
+
component.output_schema = real_schema
|
|
159
|
+
|
|
160
|
+
|
|
136
161
|
def _get_server_instructions(openapi_spec: dict[str, Any]) -> str | None:
|
|
137
162
|
"""Read backend-owned MCP server instructions from OpenAPI root extensions."""
|
|
138
163
|
instructions = openapi_spec.get(X_NOCFO_MCP_SERVER_INSTRUCTIONS)
|
|
@@ -200,6 +225,7 @@ def create_server(
|
|
|
200
225
|
from fastmcp import FastMCP
|
|
201
226
|
|
|
202
227
|
opts = options or MCPServerOptions()
|
|
228
|
+
|
|
203
229
|
if opts.auth_mode == "oauth":
|
|
204
230
|
client = _create_oauth_client(
|
|
205
231
|
config,
|
|
@@ -228,6 +254,7 @@ def create_server(
|
|
|
228
254
|
def component_mapper(route: Any, component: Any) -> None:
|
|
229
255
|
apply_mcp_namespace_names(route, component)
|
|
230
256
|
apply_mcp_operation_metadata(route, component)
|
|
257
|
+
restore_openapi_output_schema(route, component)
|
|
231
258
|
if opts.auth_mode == "oauth" and isinstance(
|
|
232
259
|
component, (Tool, OpenAPIResource, OpenAPIResourceTemplate)
|
|
233
260
|
):
|
|
@@ -245,7 +272,7 @@ def create_server(
|
|
|
245
272
|
middleware=[MCPToolErrorMiddleware()],
|
|
246
273
|
route_maps=MCP_OPENAPI_ROUTE_MAPS,
|
|
247
274
|
mcp_component_fn=component_mapper,
|
|
248
|
-
validate_output=
|
|
275
|
+
validate_output=False,
|
|
249
276
|
)
|
|
250
277
|
|
|
251
278
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|