nocfo-cli 1.4.5__tar.gz → 1.4.7__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.4.5 → nocfo_cli-1.4.7}/PKG-INFO +1 -1
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/pyproject.toml +1 -1
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/contract_validation.py +5 -1
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/middleware.py +2 -16
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/server.py +27 -41
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/LICENSE +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/README.md +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/__init__.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/api_client.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/__init__.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/app.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/files.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/products.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/schema.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/user.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/context.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/output.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/config.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/__init__.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/auth.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/http_error_capture.py +0 -0
- {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/openapi.py +0 -0
|
@@ -11,6 +11,7 @@ from fastmcp.server.providers.openapi import OpenAPIProvider
|
|
|
11
11
|
|
|
12
12
|
from nocfo_toolkit.mcp.server import (
|
|
13
13
|
MCP_OPENAPI_ROUTE_MAPS,
|
|
14
|
+
_client_event_hooks,
|
|
14
15
|
apply_mcp_operation_metadata,
|
|
15
16
|
apply_mcp_namespace_names,
|
|
16
17
|
restore_openapi_output_schema,
|
|
@@ -160,7 +161,10 @@ def validate_openapi_mcp_contract(
|
|
|
160
161
|
filtered_spec
|
|
161
162
|
)
|
|
162
163
|
|
|
163
|
-
client = httpx.AsyncClient(
|
|
164
|
+
client = httpx.AsyncClient(
|
|
165
|
+
base_url=_get_base_url(openapi_spec),
|
|
166
|
+
event_hooks=_client_event_hooks(),
|
|
167
|
+
)
|
|
164
168
|
try:
|
|
165
169
|
provider = OpenAPIProvider(
|
|
166
170
|
openapi_spec=filtered_spec,
|
|
@@ -83,23 +83,9 @@ class MCPToolErrorMiddleware(Middleware):
|
|
|
83
83
|
normalized.get("summary"),
|
|
84
84
|
)
|
|
85
85
|
raise ToolError(json.dumps(normalized, ensure_ascii=True)) from exc
|
|
86
|
-
except Exception
|
|
86
|
+
except Exception:
|
|
87
87
|
logger.exception("MCP tool call crashed tool=%s", tool_name)
|
|
88
|
-
|
|
89
|
-
if captured:
|
|
90
|
-
normalized = normalize_http_error(
|
|
91
|
-
tool_name=tool_name,
|
|
92
|
-
status_code=captured.get("status_code"),
|
|
93
|
-
payload=captured.get("payload"),
|
|
94
|
-
fallback_message=f"{type(exc).__name__}: {exc}",
|
|
95
|
-
)
|
|
96
|
-
else:
|
|
97
|
-
normalized = {
|
|
98
|
-
"tool": tool_name,
|
|
99
|
-
"error_type": "tool_error",
|
|
100
|
-
"summary": f"{type(exc).__name__}: {exc}",
|
|
101
|
-
}
|
|
102
|
-
raise ToolError(json.dumps(normalized, ensure_ascii=True)) from exc
|
|
88
|
+
raise
|
|
103
89
|
finally:
|
|
104
90
|
clear_last_http_error()
|
|
105
91
|
|
|
@@ -15,7 +15,6 @@ from fastmcp.server.providers.openapi.components import (
|
|
|
15
15
|
)
|
|
16
16
|
from fastmcp.server.providers.openapi.routing import MCPType, RouteMap
|
|
17
17
|
from fastmcp.tools.tool import Tool
|
|
18
|
-
from mcp import types as mcp_types
|
|
19
18
|
|
|
20
19
|
from nocfo_toolkit.config import AUTH_HEADER_SCHEME, ToolkitConfig
|
|
21
20
|
from nocfo_toolkit.mcp.auth import (
|
|
@@ -50,6 +49,8 @@ MCP_OPENAPI_ROUTE_MAPS: list[RouteMap] = [
|
|
|
50
49
|
X_MCP_NAMESPACE = "x-mcp-namespace"
|
|
51
50
|
_X_MCP_PREFIX = "x-mcp-"
|
|
52
51
|
X_NOCFO_MCP_SERVER_INSTRUCTIONS = "x-nocfo-mcp-server-instructions"
|
|
52
|
+
MCP_RUNTIME_CONTRACT_HEADER = "X-Nocfo-MCP-Contract"
|
|
53
|
+
MCP_RUNTIME_CONTRACT_VALUE = "1"
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def _normalize_mcp_namespace_token(value: str) -> str:
|
|
@@ -126,36 +127,6 @@ def apply_mcp_operation_metadata(route: Any, component: Any) -> None:
|
|
|
126
127
|
component.meta = meta
|
|
127
128
|
|
|
128
129
|
|
|
129
|
-
def patch_mcp_runtime_output_validation(server: FastMCP) -> None:
|
|
130
|
-
"""Disable SDK output validation at runtime without changing listed schemas.
|
|
131
|
-
|
|
132
|
-
MCP SDK validates tool outputs against ``outputSchema`` unconditionally in the
|
|
133
|
-
low-level ``call_tool`` handler. We patch the low-level lookup used only by that
|
|
134
|
-
validation path to return a cloned tool definition with ``outputSchema=None``.
|
|
135
|
-
``tools/list`` responses are unaffected and keep the original schema intact.
|
|
136
|
-
"""
|
|
137
|
-
lowlevel = getattr(server, "_mcp_server", None)
|
|
138
|
-
if lowlevel is None:
|
|
139
|
-
return
|
|
140
|
-
if getattr(lowlevel, "_nocfo_runtime_output_validation_patched", False):
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
original_get_cached_tool_definition = lowlevel._get_cached_tool_definition
|
|
144
|
-
|
|
145
|
-
async def _get_cached_tool_definition_without_output_schema(
|
|
146
|
-
tool_name: str,
|
|
147
|
-
) -> mcp_types.Tool | None:
|
|
148
|
-
tool = await original_get_cached_tool_definition(tool_name)
|
|
149
|
-
if tool is None or tool.outputSchema is None:
|
|
150
|
-
return tool
|
|
151
|
-
return tool.model_copy(update={"outputSchema": None})
|
|
152
|
-
|
|
153
|
-
lowlevel._get_cached_tool_definition = (
|
|
154
|
-
_get_cached_tool_definition_without_output_schema
|
|
155
|
-
)
|
|
156
|
-
lowlevel._nocfo_runtime_output_validation_patched = True
|
|
157
|
-
|
|
158
|
-
|
|
159
130
|
def restore_openapi_output_schema(route: Any, component: Any) -> None:
|
|
160
131
|
"""Restore the real OpenAPI output schema on tools for agent-facing metadata.
|
|
161
132
|
|
|
@@ -163,11 +134,7 @@ def restore_openapi_output_schema(route: Any, component: Any) -> None:
|
|
|
163
134
|
permissive fallback so that runtime responses are never rejected. This
|
|
164
135
|
function re-derives the accurate schema from the route and writes it back
|
|
165
136
|
onto the tool so that ``tools/list`` still exposes precise type information
|
|
166
|
-
to the agent.
|
|
167
|
-
|
|
168
|
-
Runtime output validation bypass is handled separately in
|
|
169
|
-
:func:`patch_mcp_runtime_output_validation`, so the listed schema can remain
|
|
170
|
-
fully backend-authored (including enums and other constraints).
|
|
137
|
+
to the agent — without enforcing it at call time.
|
|
171
138
|
"""
|
|
172
139
|
if not isinstance(component, Tool):
|
|
173
140
|
return
|
|
@@ -192,6 +159,27 @@ def _get_server_instructions(openapi_spec: dict[str, Any]) -> str | None:
|
|
|
192
159
|
return cleaned or None
|
|
193
160
|
|
|
194
161
|
|
|
162
|
+
def _request_requires_mcp_runtime_contract_header(request: httpx.Request) -> bool:
|
|
163
|
+
path = str(request.url.path or "").strip()
|
|
164
|
+
return path.startswith("/v1/") and not path.startswith("/v1/mcp/")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _inject_mcp_runtime_contract_header(request: httpx.Request) -> None:
|
|
168
|
+
if not _request_requires_mcp_runtime_contract_header(request):
|
|
169
|
+
return
|
|
170
|
+
request.headers.setdefault(
|
|
171
|
+
MCP_RUNTIME_CONTRACT_HEADER,
|
|
172
|
+
MCP_RUNTIME_CONTRACT_VALUE,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _client_event_hooks() -> dict[str, list[Any]]:
|
|
177
|
+
return {
|
|
178
|
+
"request": [_inject_mcp_runtime_contract_header],
|
|
179
|
+
"response": [capture_http_error_response],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
195
183
|
@dataclass(frozen=True)
|
|
196
184
|
class MCPServerOptions:
|
|
197
185
|
"""MCP server configuration options."""
|
|
@@ -219,7 +207,7 @@ def _create_pat_client(
|
|
|
219
207
|
base_url=config.base_url,
|
|
220
208
|
headers={"Authorization": f"{AUTH_HEADER_SCHEME} {resolved_token}"},
|
|
221
209
|
timeout=timeout_seconds,
|
|
222
|
-
event_hooks=
|
|
210
|
+
event_hooks=_client_event_hooks(),
|
|
223
211
|
)
|
|
224
212
|
|
|
225
213
|
|
|
@@ -237,7 +225,7 @@ def _create_oauth_client(
|
|
|
237
225
|
refresh_skew_seconds=refresh_skew_seconds,
|
|
238
226
|
),
|
|
239
227
|
timeout=timeout_seconds,
|
|
240
|
-
event_hooks=
|
|
228
|
+
event_hooks=_client_event_hooks(),
|
|
241
229
|
)
|
|
242
230
|
|
|
243
231
|
|
|
@@ -289,7 +277,7 @@ def create_server(
|
|
|
289
277
|
required_scopes=opts.required_scopes,
|
|
290
278
|
)
|
|
291
279
|
|
|
292
|
-
|
|
280
|
+
return FastMCP.from_openapi(
|
|
293
281
|
openapi_spec=filtered_spec,
|
|
294
282
|
client=client,
|
|
295
283
|
name=opts.name,
|
|
@@ -300,8 +288,6 @@ def create_server(
|
|
|
300
288
|
mcp_component_fn=component_mapper,
|
|
301
289
|
validate_output=False,
|
|
302
290
|
)
|
|
303
|
-
patch_mcp_runtime_output_validation(server)
|
|
304
|
-
return server
|
|
305
291
|
|
|
306
292
|
|
|
307
293
|
def run_server(
|
|
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
|