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.
Files changed (34) hide show
  1. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/PKG-INFO +1 -1
  2. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/pyproject.toml +1 -1
  3. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/contract_validation.py +5 -1
  4. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/middleware.py +2 -16
  5. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/server.py +27 -41
  6. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/LICENSE +0 -0
  7. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/README.md +0 -0
  8. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/__init__.py +0 -0
  9. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/api_client.py +0 -0
  10. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/__init__.py +0 -0
  11. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/app.py +0 -0
  12. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
  13. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
  14. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
  15. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
  16. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
  17. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
  18. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
  19. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/files.py +0 -0
  20. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
  21. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/products.py +0 -0
  22. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
  23. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
  24. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/schema.py +0 -0
  25. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
  26. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/user.py +0 -0
  27. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/context.py +0 -0
  28. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/output.py +0 -0
  29. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/config.py +0 -0
  30. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/__init__.py +0 -0
  31. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/auth.py +0 -0
  32. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
  33. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/http_error_capture.py +0 -0
  34. {nocfo_cli-1.4.5 → nocfo_cli-1.4.7}/src/nocfo_toolkit/openapi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nocfo-cli
3
- Version: 1.4.5
3
+ Version: 1.4.7
4
4
  Summary: NoCFO CLI, MCP server, and Cursor skill toolkit.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "nocfo-cli"
7
- version = "1.4.5"
7
+ version = "1.4.7"
8
8
  description = "NoCFO CLI, MCP server, and Cursor skill toolkit."
9
9
  authors = ["NoCFO"]
10
10
  readme = "README.md"
@@ -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(base_url=_get_base_url(openapi_spec))
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 as exc:
86
+ except Exception:
87
87
  logger.exception("MCP tool call crashed tool=%s", tool_name)
88
- captured = get_last_http_error()
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={"response": [capture_http_error_response]},
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={"response": [capture_http_error_response]},
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
- server = FastMCP.from_openapi(
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