nocfo-cli 1.4.4__tar.gz → 1.4.6__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.4 → nocfo_cli-1.4.6}/PKG-INFO +1 -1
  2. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/pyproject.toml +1 -1
  3. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/middleware.py +16 -2
  4. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/server.py +75 -24
  5. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/LICENSE +0 -0
  6. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/README.md +0 -0
  7. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/__init__.py +0 -0
  8. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/api_client.py +0 -0
  9. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/__init__.py +0 -0
  10. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/app.py +0 -0
  11. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
  12. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
  13. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
  14. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
  15. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
  16. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
  17. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
  18. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/files.py +0 -0
  19. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
  20. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/products.py +0 -0
  21. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
  22. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
  23. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/schema.py +0 -0
  24. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
  25. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/commands/user.py +0 -0
  26. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/context.py +0 -0
  27. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/cli/output.py +0 -0
  28. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/config.py +0 -0
  29. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/__init__.py +0 -0
  30. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/auth.py +0 -0
  31. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/contract_validation.py +0 -0
  32. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
  33. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/src/nocfo_toolkit/mcp/http_error_capture.py +0 -0
  34. {nocfo_cli-1.4.4 → nocfo_cli-1.4.6}/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.4
3
+ Version: 1.4.6
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.4"
7
+ version = "1.4.6"
8
8
  description = "NoCFO CLI, MCP server, and Cursor skill toolkit."
9
9
  authors = ["NoCFO"]
10
10
  readme = "README.md"
@@ -83,9 +83,23 @@ 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 as exc:
87
87
  logger.exception("MCP tool call crashed tool=%s", tool_name)
88
- raise
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
89
103
  finally:
90
104
  clear_last_http_error()
91
105
 
@@ -3,7 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import json
7
+ import logging
6
8
  import re
9
+ from uuid import uuid4
7
10
  from dataclasses import dataclass
8
11
  from typing import TYPE_CHECKING, Any, Literal
9
12
 
@@ -15,6 +18,7 @@ from fastmcp.server.providers.openapi.components import (
15
18
  )
16
19
  from fastmcp.server.providers.openapi.routing import MCPType, RouteMap
17
20
  from fastmcp.tools.tool import Tool
21
+ from mcp import types as mcp_types
18
22
 
19
23
  from nocfo_toolkit.config import AUTH_HEADER_SCHEME, ToolkitConfig
20
24
  from nocfo_toolkit.mcp.auth import (
@@ -37,6 +41,8 @@ from fastmcp.utilities.openapi import extract_output_schema_from_responses
37
41
  if TYPE_CHECKING:
38
42
  from fastmcp import FastMCP
39
43
 
44
+ logger = logging.getLogger(__name__)
45
+
40
46
  # Semantic mapping for MCP-tagged routes (see FastMCP OpenAPI route maps):
41
47
  # https://gofastmcp.com/integrations/openapi#custom-route-maps
42
48
  MCP_OPENAPI_ROUTE_MAPS: list[RouteMap] = [
@@ -49,6 +55,8 @@ MCP_OPENAPI_ROUTE_MAPS: list[RouteMap] = [
49
55
  X_MCP_NAMESPACE = "x-mcp-namespace"
50
56
  _X_MCP_PREFIX = "x-mcp-"
51
57
  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
60
 
53
61
 
54
62
  def _normalize_mcp_namespace_token(value: str) -> str:
@@ -125,26 +133,68 @@ def apply_mcp_operation_metadata(route: Any, component: Any) -> None:
125
133
  component.meta = meta
126
134
 
127
135
 
128
- _VALIDATION_ONLY_KEYS = frozenset({"enum", "const"})
136
+ def patch_mcp_runtime_output_validation(server: FastMCP) -> None:
137
+ """Disable SDK output validation at runtime without changing listed schemas.
129
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
130
149
 
131
- def _strip_validation_constraints(schema: Any) -> Any:
132
- """Recursively strip strict validation constraints from a JSON schema.
150
+ original_get_cached_tool_definition = lowlevel._get_cached_tool_definition
133
151
 
134
- Removes ``enum`` and ``const`` so that the MCP SDK's unconditional
135
- ``jsonschema.validate()`` on ``outputSchema`` never rejects responses
136
- containing values absent from the declared set (e.g. legacy DB values).
137
- Structural information (types, properties, descriptions) is preserved.
138
- """
139
- if isinstance(schema, dict):
140
- return {
141
- key: _strip_validation_constraints(value)
142
- for key, value in schema.items()
143
- if key not in _VALIDATION_ONLY_KEYS
144
- }
145
- if isinstance(schema, list):
146
- return [_strip_validation_constraints(item) for item in schema]
147
- return schema
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
148
198
 
149
199
 
150
200
  def restore_openapi_output_schema(route: Any, component: Any) -> None:
@@ -156,11 +206,9 @@ def restore_openapi_output_schema(route: Any, component: Any) -> None:
156
206
  onto the tool so that ``tools/list`` still exposes precise type information
157
207
  to the agent.
158
208
 
159
- The MCP SDK (``mcp.server.lowlevel``) unconditionally validates
160
- ``structured_content`` against ``outputSchema`` via ``jsonschema``.
161
- To prevent validation failures on legacy or unexpected values the
162
- restored schema has strict value constraints (``enum``, ``const``)
163
- stripped while keeping full structural and type information.
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).
164
212
  """
165
213
  if not isinstance(component, Tool):
166
214
  return
@@ -173,7 +221,7 @@ def restore_openapi_output_schema(route: Any, component: Any) -> None:
173
221
  getattr(route, "openapi_version", None),
174
222
  )
175
223
  if real_schema is not None:
176
- component.output_schema = _strip_validation_constraints(real_schema)
224
+ component.output_schema = real_schema
177
225
 
178
226
 
179
227
  def _get_server_instructions(openapi_spec: dict[str, Any]) -> str | None:
@@ -282,7 +330,7 @@ def create_server(
282
330
  required_scopes=opts.required_scopes,
283
331
  )
284
332
 
285
- return FastMCP.from_openapi(
333
+ server = FastMCP.from_openapi(
286
334
  openapi_spec=filtered_spec,
287
335
  client=client,
288
336
  name=opts.name,
@@ -293,6 +341,9 @@ def create_server(
293
341
  mcp_component_fn=component_mapper,
294
342
  validate_output=False,
295
343
  )
344
+ patch_mcp_runtime_output_validation(server)
345
+ patch_mcp_output_error_masking(server)
346
+ return server
296
347
 
297
348
 
298
349
  def run_server(
File without changes
File without changes