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.
Files changed (34) hide show
  1. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/PKG-INFO +1 -1
  2. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/pyproject.toml +1 -1
  3. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/contract_validation.py +4 -2
  4. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/http_error_capture.py +18 -8
  5. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/server.py +28 -1
  6. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/LICENSE +0 -0
  7. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/README.md +0 -0
  8. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/__init__.py +0 -0
  9. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/api_client.py +0 -0
  10. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/__init__.py +0 -0
  11. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/app.py +0 -0
  12. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
  13. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
  14. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
  15. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
  16. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
  17. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
  18. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
  19. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/files.py +0 -0
  20. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
  21. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/products.py +0 -0
  22. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
  23. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
  24. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/schema.py +0 -0
  25. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
  26. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/commands/user.py +0 -0
  27. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/context.py +0 -0
  28. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/cli/output.py +0 -0
  29. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/config.py +0 -0
  30. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/__init__.py +0 -0
  31. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/auth.py +0 -0
  32. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
  33. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/src/nocfo_toolkit/mcp/middleware.py +0 -0
  34. {nocfo_cli-1.3.0 → nocfo_cli-1.4.1}/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.3.0
3
+ Version: 1.4.1
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.3.0"
7
+ version = "1.4.1"
8
8
  description = "NoCFO CLI, MCP server, and Cursor skill toolkit."
9
9
  authors = ["NoCFO"]
10
10
  readme = "README.md"
@@ -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 = True,
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 = True,
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 non-success HTTP response."""
24
+ """Store status/payload for the latest HTTP response.
25
25
 
26
- if response.status_code < 400:
27
- _LAST_HTTP_ERROR.set(None)
28
- return
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
- pass
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 (ValueError, httpx.ResponseNotRead):
38
- payload = (response.text or "").strip()[:1000] or None
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=True,
275
+ validate_output=False,
249
276
  )
250
277
 
251
278
 
File without changes
File without changes