nocfo-cli 1.4.6__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.6 → nocfo_cli-1.4.7}/PKG-INFO +1 -1
  2. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/pyproject.toml +1 -1
  3. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/contract_validation.py +5 -1
  4. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/middleware.py +2 -16
  5. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/server.py +27 -83
  6. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/LICENSE +0 -0
  7. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/README.md +0 -0
  8. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/__init__.py +0 -0
  9. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/api_client.py +0 -0
  10. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/__init__.py +0 -0
  11. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/app.py +0 -0
  12. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
  13. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
  14. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
  15. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
  16. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
  17. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
  18. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
  19. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/files.py +0 -0
  20. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
  21. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/products.py +0 -0
  22. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
  23. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
  24. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/schema.py +0 -0
  25. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
  26. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/commands/user.py +0 -0
  27. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/context.py +0 -0
  28. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/cli/output.py +0 -0
  29. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/config.py +0 -0
  30. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/__init__.py +0 -0
  31. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/auth.py +0 -0
  32. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
  33. {nocfo_cli-1.4.6 → nocfo_cli-1.4.7}/src/nocfo_toolkit/mcp/http_error_capture.py +0 -0
  34. {nocfo_cli-1.4.6 → 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.6
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.6"
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
 
@@ -3,10 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import json
7
- import logging
8
6
  import re
9
- from uuid import uuid4
10
7
  from dataclasses import dataclass
11
8
  from typing import TYPE_CHECKING, Any, Literal
12
9
 
@@ -18,7 +15,6 @@ from fastmcp.server.providers.openapi.components import (
18
15
  )
19
16
  from fastmcp.server.providers.openapi.routing import MCPType, RouteMap
20
17
  from fastmcp.tools.tool import Tool
21
- from mcp import types as mcp_types
22
18
 
23
19
  from nocfo_toolkit.config import AUTH_HEADER_SCHEME, ToolkitConfig
24
20
  from nocfo_toolkit.mcp.auth import (
@@ -41,8 +37,6 @@ from fastmcp.utilities.openapi import extract_output_schema_from_responses
41
37
  if TYPE_CHECKING:
42
38
  from fastmcp import FastMCP
43
39
 
44
- logger = logging.getLogger(__name__)
45
-
46
40
  # Semantic mapping for MCP-tagged routes (see FastMCP OpenAPI route maps):
47
41
  # https://gofastmcp.com/integrations/openapi#custom-route-maps
48
42
  MCP_OPENAPI_ROUTE_MAPS: list[RouteMap] = [
@@ -55,8 +49,8 @@ MCP_OPENAPI_ROUTE_MAPS: list[RouteMap] = [
55
49
  X_MCP_NAMESPACE = "x-mcp-namespace"
56
50
  _X_MCP_PREFIX = "x-mcp-"
57
51
  X_NOCFO_MCP_SERVER_INSTRUCTIONS = "x-nocfo-mcp-server-instructions"
58
- _OUTPUT_VALIDATION_ERROR_PREFIX = "Output validation error:"
59
- _GENERIC_TOOL_EXECUTION_ERROR = "Error occurred during tool execution"
52
+ MCP_RUNTIME_CONTRACT_HEADER = "X-Nocfo-MCP-Contract"
53
+ MCP_RUNTIME_CONTRACT_VALUE = "1"
60
54
 
61
55
 
62
56
  def _normalize_mcp_namespace_token(value: str) -> str:
@@ -133,70 +127,6 @@ def apply_mcp_operation_metadata(route: Any, component: Any) -> None:
133
127
  component.meta = meta
134
128
 
135
129
 
136
- def patch_mcp_runtime_output_validation(server: FastMCP) -> None:
137
- """Disable SDK output validation at runtime without changing listed schemas.
138
-
139
- MCP SDK validates tool outputs against ``outputSchema`` unconditionally in the
140
- low-level ``call_tool`` handler. We patch the low-level lookup used only by that
141
- validation path to return a cloned tool definition with ``outputSchema=None``.
142
- ``tools/list`` responses are unaffected and keep the original schema intact.
143
- """
144
- lowlevel = getattr(server, "_mcp_server", None)
145
- if lowlevel is None:
146
- return
147
- if getattr(lowlevel, "_nocfo_runtime_output_validation_patched", False):
148
- return
149
-
150
- original_get_cached_tool_definition = lowlevel._get_cached_tool_definition
151
-
152
- async def _get_cached_tool_definition_without_output_schema(
153
- tool_name: str,
154
- ) -> mcp_types.Tool | None:
155
- tool = await original_get_cached_tool_definition(tool_name)
156
- if tool is None or tool.outputSchema is None:
157
- return tool
158
- return tool.model_copy(update={"outputSchema": None})
159
-
160
- lowlevel._get_cached_tool_definition = (
161
- _get_cached_tool_definition_without_output_schema
162
- )
163
- lowlevel._nocfo_runtime_output_validation_patched = True
164
-
165
-
166
- def patch_mcp_output_error_masking(server: FastMCP) -> None:
167
- """Mask verbose output-validation errors and log full details server-side."""
168
- lowlevel = getattr(server, "_mcp_server", None)
169
- if lowlevel is None:
170
- return
171
- if getattr(lowlevel, "_nocfo_output_validation_error_mask_patched", False):
172
- return
173
-
174
- original_make_error_result = lowlevel._make_error_result
175
-
176
- def _make_error_result_with_masking(error_message: str):
177
- if isinstance(error_message, str) and error_message.startswith(
178
- _OUTPUT_VALIDATION_ERROR_PREFIX
179
- ):
180
- request_id = f"req_{uuid4().hex[:24]}"
181
- logger.error(
182
- "MCP output validation failure request_id=%s detail=%s",
183
- request_id,
184
- error_message,
185
- )
186
- client_payload = json.dumps(
187
- {
188
- "error": _GENERIC_TOOL_EXECUTION_ERROR,
189
- "request_id": request_id,
190
- },
191
- ensure_ascii=True,
192
- )
193
- return original_make_error_result(client_payload)
194
- return original_make_error_result(error_message)
195
-
196
- lowlevel._make_error_result = _make_error_result_with_masking
197
- lowlevel._nocfo_output_validation_error_mask_patched = True
198
-
199
-
200
130
  def restore_openapi_output_schema(route: Any, component: Any) -> None:
201
131
  """Restore the real OpenAPI output schema on tools for agent-facing metadata.
202
132
 
@@ -204,11 +134,7 @@ def restore_openapi_output_schema(route: Any, component: Any) -> None:
204
134
  permissive fallback so that runtime responses are never rejected. This
205
135
  function re-derives the accurate schema from the route and writes it back
206
136
  onto the tool so that ``tools/list`` still exposes precise type information
207
- to the agent.
208
-
209
- Runtime output validation bypass is handled separately in
210
- :func:`patch_mcp_runtime_output_validation`, so the listed schema can remain
211
- fully backend-authored (including enums and other constraints).
137
+ to the agent — without enforcing it at call time.
212
138
  """
213
139
  if not isinstance(component, Tool):
214
140
  return
@@ -233,6 +159,27 @@ def _get_server_instructions(openapi_spec: dict[str, Any]) -> str | None:
233
159
  return cleaned or None
234
160
 
235
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
+
236
183
  @dataclass(frozen=True)
237
184
  class MCPServerOptions:
238
185
  """MCP server configuration options."""
@@ -260,7 +207,7 @@ def _create_pat_client(
260
207
  base_url=config.base_url,
261
208
  headers={"Authorization": f"{AUTH_HEADER_SCHEME} {resolved_token}"},
262
209
  timeout=timeout_seconds,
263
- event_hooks={"response": [capture_http_error_response]},
210
+ event_hooks=_client_event_hooks(),
264
211
  )
265
212
 
266
213
 
@@ -278,7 +225,7 @@ def _create_oauth_client(
278
225
  refresh_skew_seconds=refresh_skew_seconds,
279
226
  ),
280
227
  timeout=timeout_seconds,
281
- event_hooks={"response": [capture_http_error_response]},
228
+ event_hooks=_client_event_hooks(),
282
229
  )
283
230
 
284
231
 
@@ -330,7 +277,7 @@ def create_server(
330
277
  required_scopes=opts.required_scopes,
331
278
  )
332
279
 
333
- server = FastMCP.from_openapi(
280
+ return FastMCP.from_openapi(
334
281
  openapi_spec=filtered_spec,
335
282
  client=client,
336
283
  name=opts.name,
@@ -341,9 +288,6 @@ def create_server(
341
288
  mcp_component_fn=component_mapper,
342
289
  validate_output=False,
343
290
  )
344
- patch_mcp_runtime_output_validation(server)
345
- patch_mcp_output_error_masking(server)
346
- return server
347
291
 
348
292
 
349
293
  def run_server(
File without changes
File without changes