Skip to content

Commit 5a0d92f

Browse files
fix(langgraph): stop concatenating server/tool into connector_type (#216)
* fix(langgraph): stop concatenating server/tool into connector_type MCPCheckInputRequest/MCPCheckOutputRequest now carry a genuine two-field (server, tool) identity contract (epic #2905, platform sub-issue #2904). Client.mcp_check_input/mcp_check_output gain a `tool` parameter sent as its own wire field instead of being folded into `connector_type`. mcp_tool_interceptor's default connector-type resolution now sends connector_type=request.server_name and tool=request.name as two separate values instead of the dotted "server.tool" string, and this holds even when a caller supplies a custom connector_type_fn. In tool_output_wrapper, ToolNode's call_request carries no server/origin info to improve connector_type's default beyond the bare tool name, but the tool name itself is now always forwarded via the new `tool` field rather than only living inside connector_type. Signed-off-by: Gregory Zak <gzak@getaxonflow.com> * fix(langgraph)!: de-concatenate server/tool as breaking major (epic #2905) Mark the LangGraph server/tool de-concatenation as a BREAKING change (the connector_type wire value changes from "{server}.{tool}" to the bare server name) and document the migration: - Changelog: move to "Changed (BREAKING)", require a major SDK bump, add the policy re-scope note (connector_type_fn is the compat lever), the custom-fn statement-shape change, the missing-server fail-closed edge, and the platform-version pins (tool consumed on check-input at v9.10.0+; check-output tracked by #2955). - Add the missing-server edge test: empty server_name -> connector_type="" -> platform 400 -> ConnectorError (fail-closed); document + pin the wire. - Reconcile the shared client.py/types.py/wire_shape_baseline.json hunks with PR #217 to byte-identical, corrected text (#2916/c8df2006b merged, first released in v9.10.0); thread `tool` through the check_tool_input/ check_tool_output aliases too. Declare the #216-before-#217 merge order. - Regenerate .lint_baselines/falsey_clobber.json (line-shift only; CI green). Refs #2906, epic #2905. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> * docs(changelog): add blank line before ### Added (MD022) Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> * release: finalize v9.0.0 — version bump + clean [9.0.0] changelog Bump pyproject.toml + axonflow/_version.py to 9.0.0 and finalize the tool-identity content under ## [9.0.0] - 2026-07-18 (banner dropped). The LangGraph and Computer Use de-concatenation is described in a single combined entry (byte-identical across #216/#217 — merge #216 first). Changelog scrubbed of internal issue/PR numbers and internal review terminology; minimum platform stated as versions (check-input tool = platform v9.10.0+, response-plane tool = platform v9.11.0+). Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> --------- Signed-off-by: Gregory Zak <gzak@getaxonflow.com> Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com> Co-authored-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
1 parent 5d1809e commit 5a0d92f

11 files changed

Lines changed: 512 additions & 40 deletions

File tree

.lint_baselines/falsey_clobber.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,33 @@
1111
"axonflow/adapters/langchain.py:160:15",
1212
"axonflow/adapters/langchain.py:160:64",
1313
"axonflow/adapters/langchain.py:169:16",
14-
"axonflow/adapters/langgraph.py:569:33",
15-
"axonflow/adapters/langgraph.py:583:43",
16-
"axonflow/adapters/langgraph.py:597:20",
17-
"axonflow/adapters/langgraph.py:654:33",
18-
"axonflow/adapters/langgraph.py:668:20",
19-
"axonflow/adapters/langgraph.py:692:20",
14+
"axonflow/adapters/langgraph.py:573:33",
15+
"axonflow/adapters/langgraph.py:592:43",
16+
"axonflow/adapters/langgraph.py:607:20",
17+
"axonflow/adapters/langgraph.py:670:33",
18+
"axonflow/adapters/langgraph.py:686:20",
19+
"axonflow/adapters/langgraph.py:711:20",
2020
"axonflow/adapters/langgraph_wrapper.py:374:19",
2121
"axonflow/adapters/tool_wrapper.py:176:20",
2222
"axonflow/adapters/tool_wrapper.py:190:20",
2323
"axonflow/adapters/tool_wrapper.py:208:20",
2424
"axonflow/adapters/tool_wrapper.py:220:20",
2525
"axonflow/client.py:1111:16",
2626
"axonflow/client.py:1188:16",
27-
"axonflow/client.py:1669:37",
28-
"axonflow/client.py:1710:18",
29-
"axonflow/client.py:1768:37",
30-
"axonflow/client.py:2292:24",
31-
"axonflow/client.py:2313:33",
32-
"axonflow/client.py:2314:31",
33-
"axonflow/client.py:2326:25",
34-
"axonflow/client.py:2387:28",
35-
"axonflow/client.py:2428:69",
27+
"axonflow/client.py:1686:37",
28+
"axonflow/client.py:1727:18",
29+
"axonflow/client.py:1785:37",
30+
"axonflow/client.py:2309:24",
31+
"axonflow/client.py:2330:33",
32+
"axonflow/client.py:2331:31",
33+
"axonflow/client.py:2343:25",
34+
"axonflow/client.py:2404:28",
35+
"axonflow/client.py:2445:69",
3636
"axonflow/client.py:300:14",
3737
"axonflow/client.py:305:24",
3838
"axonflow/client.py:306:20",
3939
"axonflow/client.py:529:44",
40-
"axonflow/client.py:6469:25",
40+
"axonflow/client.py:6486:25",
4141
"axonflow/client.py:845:20",
4242
"axonflow/client.py:931:20",
4343
"axonflow/execution.py:205:19",

CHANGELOG.md

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
## [Unreleased]
1313

14+
## [9.0.0] - 2026-07-18
15+
16+
### Changed (BREAKING)
17+
18+
- **The LangGraph and Computer Use adapters now report the (server, tool)
19+
identity as two separate wire fields instead of concatenating them into
20+
`connector_type`.** `mcp_check_input`/`mcp_check_output` (and their
21+
`check_tool_input`/`check_tool_output` aliases) gain an optional `tool`
22+
parameter, sent alongside `connector_type` on the wire — the platform's
23+
two-field (server, tool) identity contract. The tool name is never folded
24+
back into `connector_type`.
25+
26+
- **LangGraph** (`mcp_tool_interceptor`) now sends
27+
`connector_type = request.server_name` and `tool = request.name` instead
28+
of `f"{server_name}.{name}"`; the default `connector_type_fn` returns the
29+
bare `server_name`. `connector_type_fn` is the compatibility lever — a
30+
caller can restore any prior `connector_type` value (including the old
31+
concatenated form, `lambda req: f"{req.server_name}.{req.name}"`) without
32+
losing the separate `tool` field. The human-readable `statement` is now
33+
`"{connector_type}.{tool}(args)"` built from the resolved connector type;
34+
with a custom `connector_type_fn` its shape shifts from `"{custom}(args)"`
35+
to `"{custom}.{tool}(args)"`. With the default resolver, a tool whose
36+
`server_name` is empty sends `connector_type=""`, which the platform
37+
rejects with HTTP 400 — the call raises `ConnectorError` and is blocked
38+
(fail-closed); supply a `connector_type_fn` for server-less MCP tools.
39+
40+
- **Computer Use** (`ComputerUseGovernor`) now sends the constant
41+
`connector_type = "computer_use"` and `tool =` the tool name (`computer`,
42+
`bash`, `text_editor`). The action (e.g. `left_click`) is preserved inside
43+
the serialized `statement`, not in `tool` (and not in `operation`, which
44+
is constrained to `{query, execute}`). This adapter has **no**
45+
`connector_type_fn` escape hatch, so re-scoping policies is the only
46+
migration path.
47+
48+
**Migration.** Policies or per-connector settings matching the old
49+
concatenated value — e.g. `connector_type == "filesystem.read_file"` or
50+
`"computer_use.left_click"` — stop matching after upgrade. Re-scope them to
51+
match `connector_type` (the bare server name, or `"computer_use"`) together
52+
with the `tool` field (e.g. `tool == "read_file"`).
53+
54+
**Minimum platform.** The `tool` field is consumed on `POST
55+
/api/v1/mcp/check-input` by **AxonFlow platform v9.10.0+**. On older
56+
platforms it is silently dropped and identity degrades to the bare
57+
`connector_type` — upgrade the platform to v9.10.0+ before adopting this SDK
58+
major. Response-plane (`check-output`) `tool` scoping requires **AxonFlow
59+
platform v9.11.0+**; until then the SDK sends it forward-compatibly and older
60+
platforms ignore it.
61+
1462
### Added
1563

16-
- **`AuditToolCallRequest.caller_name`** (getaxonflow/axonflow-enterprise#2912,
17-
sub-issue of epic #2905) — identifies which client made a non-LLM tool
18-
call (e.g. `claude_code`, `codex`, `cursor`, `openclaw`). Replaces the
19-
misleadingly-named `tool_type` field, which every real caller actually
20-
used to identify the calling client rather than any property of the
21-
tool. `tool_type` is kept as a deprecated input fallback (not removed):
22-
the server resolves `caller_name` if supplied, else the legacy
23-
`tool_type`, else a default. See
24-
`runtime-e2e/caller_name_audit/` for the real-stack proof that
25-
`caller_name` reaches `policy_details.caller_name` on the audit row.
64+
- **`AuditToolCallRequest.caller_name`** — identifies which client made a
65+
non-LLM tool call (e.g. `claude_code`, `codex`, `cursor`, `openclaw`).
66+
Replaces the misleadingly-named `tool_type` field, which every real caller
67+
actually used to identify the calling client rather than any property of the
68+
tool. `tool_type` is kept as a deprecated input fallback (not removed): the
69+
server resolves `caller_name` if supplied, else the legacy `tool_type`, else
70+
a default.
2671

2772
## [8.5.1] - 2026-07-09 — Interceptor sync bridge + async-client detection + example fixes
2873

axonflow/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Single source of truth for the AxonFlow SDK version."""
22

3-
__version__ = "8.5.1"
3+
__version__ = "9.0.0"

axonflow/adapters/langgraph.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ class MCPInterceptorOptions:
9393
9494
Attributes:
9595
connector_type_fn: Optional callable that maps an MCP request to a
96-
connector type string. Defaults to ``"{server_name}.{tool_name}"``.
96+
connector type string. Defaults to the request's ``server_name``.
97+
The tool name is always sent separately as the ``tool`` field
98+
(see :meth:`~axonflow.client.Client.mcp_check_input`) — it is no
99+
longer folded into ``connector_type``.
97100
operation: Operation type passed to ``mcp_check_input``. Defaults to
98101
``"execute"``. Set to ``"query"`` for known read-only tool calls.
99102
"""
@@ -522,11 +525,12 @@ def mcp_tool_interceptor(
522525
... tool_interceptors=[adapter.mcp_tool_interceptor()],
523526
... )
524527
525-
With custom options:
528+
With custom options (the tool name is always sent separately as
529+
``tool`` — ``connector_type_fn`` only controls ``connector_type``):
526530
527531
Example:
528532
>>> opts = MCPInterceptorOptions(
529-
... connector_type_fn=lambda req: req.server_name,
533+
... connector_type_fn=lambda req: f"prod.{req.server_name}",
530534
... operation="query",
531535
... )
532536
>>> tool_interceptors=[adapter.mcp_tool_interceptor(opts)]
@@ -564,17 +568,22 @@ def mcp_tool_interceptor(
564568
opts = options or MCPInterceptorOptions()
565569

566570
def _default_connector_type(request: Any) -> str:
567-
return f"{request.server_name}.{request.name}"
571+
return str(request.server_name)
568572

569573
resolve_connector_type = opts.connector_type_fn or _default_connector_type
570574

571575
async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any:
572576
connector_type = resolve_connector_type(request)
577+
# The tool name is a genuine second identity field (epic #2905 /
578+
# #2904) — send it as `tool`, never fold it back into
579+
# `connector_type`, even when connector_type_fn is customized.
580+
tool = request.name
573581
args_str = json.dumps(request.args, default=str) if request.args else "{}"
574-
statement = f"{connector_type}({args_str})"
582+
statement = f"{connector_type}.{tool}({args_str})"
575583

576584
pre_check = await self.client.mcp_check_input(
577585
connector_type=connector_type,
586+
tool=tool,
578587
statement=statement,
579588
operation=opts.operation,
580589
parameters=request.args,
@@ -590,6 +599,7 @@ async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any:
590599
result_str = str(result)
591600
output_check = await self.client.mcp_check_output(
592601
connector_type=connector_type,
602+
tool=tool,
593603
message=result_str,
594604
)
595605
if not output_check.allowed:
@@ -620,11 +630,12 @@ def tool_output_wrapper(
620630
>>> wrapper = adapter.tool_output_wrapper()
621631
>>> tool_node = ToolNode(tools, awrap_tool_call=wrapper)
622632
623-
With custom options:
633+
With custom options (the tool name is always sent separately as
634+
``tool`` — ``connector_type_fn`` only controls ``connector_type``):
624635
625636
Example:
626637
>>> opts = MCPInterceptorOptions(
627-
... connector_type_fn=lambda call: f"local.{call['name']}",
638+
... connector_type_fn=lambda call: "local",
628639
... )
629640
>>> tool_node = ToolNode(tools, awrap_tool_call=adapter.tool_output_wrapper(opts))
630641
@@ -648,18 +659,25 @@ def tool_output_wrapper(
648659
opts = options or MCPInterceptorOptions()
649660

650661
def _default_connector_type(call_request: dict[str, Any]) -> str:
662+
# ToolNode's call_request carries no server/origin info to
663+
# distinguish from the bare tool name — this is the best
664+
# available default. The tool name itself is never dropped: it
665+
# is always sent separately as `tool` below (epic #2905 /
666+
# #2904), even when it also ends up equal to connector_type here.
651667
name: str = call_request.get("name", "unknown_tool")
652668
return name
653669

654670
resolve_connector_type = opts.connector_type_fn or _default_connector_type
655671

656672
async def _wrapper(call_request: dict[str, Any], execute: Callable[..., Any]) -> Any:
657673
connector_type = resolve_connector_type(call_request)
674+
tool: str = call_request.get("name", "unknown_tool")
658675
args = call_request.get("args", {})
659676
statement = json.dumps(args, default=str)
660677

661678
input_check = await self.client.mcp_check_input(
662679
connector_type=connector_type,
680+
tool=tool,
663681
statement=statement,
664682
operation=opts.operation,
665683
)
@@ -685,6 +703,7 @@ async def _wrapper(call_request: dict[str, Any], execute: Callable[..., Any]) ->
685703

686704
output_check = await self.client.mcp_check_output(
687705
connector_type=connector_type,
706+
tool=tool,
688707
message=serialized,
689708
)
690709
if not output_check.allowed:

axonflow/client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,7 @@ async def mcp_check_input(
14381438
operation: str = "execute",
14391439
parameters: dict[str, Any] | None = None,
14401440
*,
1441+
tool: str | None = None,
14411442
client_id: str | None = None,
14421443
tenant_id: str | None = None,
14431444
user_id: str | None = None,
@@ -1455,6 +1456,10 @@ async def mcp_check_input(
14551456
statement: The SQL query or command to validate.
14561457
operation: Operation type - "query" or "execute" (default).
14571458
parameters: Optional query parameters.
1459+
tool: Optional tool name, distinct from ``connector_type``. Lets a
1460+
PEP report the (server, tool) identity as two separate values
1461+
instead of concatenating them into ``connector_type``
1462+
(epic #2905 / #2904).
14581463
client_id: Client identifier (overrides client config when set).
14591464
tenant_id: Tenant identifier for multi-tenant scoping.
14601465
user_id: End-user identifier for per-user policies.
@@ -1481,6 +1486,8 @@ async def mcp_check_input(
14811486
}
14821487
if parameters:
14831488
body["parameters"] = parameters
1489+
if tool:
1490+
body["tool"] = tool
14841491
# Wire-canonical scoping fields surfaced in the v6 sweep.
14851492
if client_id is not None:
14861493
body["client_id"] = client_id
@@ -1518,6 +1525,7 @@ async def check_tool_input(
15181525
operation: str = "execute",
15191526
parameters: dict[str, Any] | None = None,
15201527
*,
1528+
tool: str | None = None,
15211529
client_id: str | None = None,
15221530
tenant_id: str | None = None,
15231531
user_id: str | None = None,
@@ -1530,6 +1538,7 @@ async def check_tool_input(
15301538
statement,
15311539
operation,
15321540
parameters,
1541+
tool=tool,
15331542
client_id=client_id,
15341543
tenant_id=tenant_id,
15351544
user_id=user_id,
@@ -1545,6 +1554,7 @@ async def mcp_check_output(
15451554
metadata: dict[str, Any] | None = None,
15461555
row_count: int = 0,
15471556
*,
1557+
tool: str | None = None,
15481558
client_id: str | None = None,
15491559
tenant_id: str | None = None,
15501560
user_id: str | None = None,
@@ -1561,6 +1571,9 @@ async def mcp_check_output(
15611571
message: Execute-style response message (e.g., "5 rows affected").
15621572
metadata: Connector metadata for SQLi scanning.
15631573
row_count: Total number of rows returned.
1574+
tool: Optional tool name, distinct from ``connector_type``. Mirrors
1575+
the ``tool`` parameter on :meth:`mcp_check_input` (epic #2905 /
1576+
#2904).
15641577
client_id: Client identifier (overrides client config when set).
15651578
tenant_id: Tenant identifier for multi-tenant scoping.
15661579
user_id: End-user identifier for per-user policies.
@@ -1584,6 +1597,8 @@ async def mcp_check_output(
15841597
body["metadata"] = metadata
15851598
if row_count > 0:
15861599
body["row_count"] = row_count
1600+
if tool:
1601+
body["tool"] = tool
15871602
# Wire-canonical scoping fields surfaced in the v6 sweep.
15881603
if client_id is not None:
15891604
body["client_id"] = client_id
@@ -1618,6 +1633,7 @@ async def check_tool_output(
16181633
metadata: dict[str, Any] | None = None,
16191634
row_count: int = 0,
16201635
*,
1636+
tool: str | None = None,
16211637
client_id: str | None = None,
16221638
tenant_id: str | None = None,
16231639
user_id: str | None = None,
@@ -1630,6 +1646,7 @@ async def check_tool_output(
16301646
message,
16311647
metadata,
16321648
row_count,
1649+
tool=tool,
16331650
client_id=client_id,
16341651
tenant_id=tenant_id,
16351652
user_id=user_id,
@@ -7555,6 +7572,7 @@ def mcp_check_input(
75557572
operation: str = "execute",
75567573
parameters: dict[str, Any] | None = None,
75577574
*,
7575+
tool: str | None = None,
75587576
client_id: str | None = None,
75597577
tenant_id: str | None = None,
75607578
user_id: str | None = None,
@@ -7569,6 +7587,7 @@ def mcp_check_input(
75697587
statement,
75707588
operation,
75717589
parameters,
7590+
tool=tool,
75727591
client_id=client_id,
75737592
tenant_id=tenant_id,
75747593
user_id=user_id,
@@ -7618,6 +7637,7 @@ def mcp_check_output(
76187637
metadata: dict[str, Any] | None = None,
76197638
row_count: int = 0,
76207639
*,
7640+
tool: str | None = None,
76217641
client_id: str | None = None,
76227642
tenant_id: str | None = None,
76237643
user_id: str | None = None,
@@ -7631,6 +7651,7 @@ def mcp_check_output(
76317651
message,
76327652
metadata,
76337653
row_count,
7654+
tool=tool,
76347655
client_id=client_id,
76357656
tenant_id=tenant_id,
76367657
user_id=user_id,
@@ -7645,6 +7666,7 @@ def check_tool_input(
76457666
operation: str = "execute",
76467667
parameters: dict[str, Any] | None = None,
76477668
*,
7669+
tool: str | None = None,
76487670
client_id: str | None = None,
76497671
tenant_id: str | None = None,
76507672
user_id: str | None = None,
@@ -7658,6 +7680,7 @@ def check_tool_input(
76587680
statement,
76597681
operation,
76607682
parameters,
7683+
tool=tool,
76617684
client_id=client_id,
76627685
tenant_id=tenant_id,
76637686
user_id=user_id,
@@ -7674,6 +7697,7 @@ def check_tool_output(
76747697
metadata: dict[str, Any] | None = None,
76757698
row_count: int = 0,
76767699
*,
7700+
tool: str | None = None,
76777701
client_id: str | None = None,
76787702
tenant_id: str | None = None,
76797703
user_id: str | None = None,
@@ -7687,6 +7711,7 @@ def check_tool_output(
76877711
message,
76887712
metadata,
76897713
row_count,
7714+
tool=tool,
76907715
client_id=client_id,
76917716
tenant_id=tenant_id,
76927717
user_id=user_id,

0 commit comments

Comments
 (0)