Skip to content

Commit 319f2bb

Browse files
committed
fix(tools): Fix serialization for Claude Code passing unexpected serialized inputs
Issue: APPAI-89
1 parent 3f55838 commit 319f2bb

File tree

6 files changed

+344
-16
lines changed

6 files changed

+344
-16
lines changed

packages/developer_mcp_server/src/developer_mcp_server/register_tools.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from fastmcp import FastMCP
2+
from gg_api_core.schema_utils import compress_pydantic_model_schema
23
from gg_api_core.tools.find_current_source_id import find_current_source_id
34
from gg_api_core.tools.generate_honey_token import generate_honeytoken
45
from gg_api_core.tools.list_honey_tokens import list_honeytokens
@@ -45,15 +46,19 @@
4546

4647

4748
def register_developer_tools(mcp: FastMCP):
48-
mcp.tool(
49+
# Register tools with Pydantic model parameters and compress their schemas
50+
# to work around Claude Code bug that serializes params as JSON strings
51+
52+
remediate_tool = mcp.tool(
4953
remediate_secret_incidents,
5054
description="Find and fix secrets in the current repository using exact match locations (file paths, line numbers, character indices). "
5155
"This tool leverages the occurrences API to provide precise remediation instructions without needing to search for secrets in files. "
5256
"By default, this only shows incidents assigned to the current user. Pass mine=False to get all incidents related to this repo.",
5357
required_scopes=["incidents:read", "sources:read"],
5458
)
59+
remediate_tool.parameters = compress_pydantic_model_schema(remediate_tool.parameters)
5560

56-
mcp.tool(
61+
scan_tool = mcp.tool(
5762
scan_secrets,
5863
description="""
5964
Scan multiple content items for secrets and policy breaks.
@@ -65,23 +70,27 @@ def register_developer_tools(mcp: FastMCP):
6570
""",
6671
required_scopes=["scan"],
6772
)
73+
scan_tool.parameters = compress_pydantic_model_schema(scan_tool.parameters)
6874

69-
mcp.tool(
75+
list_incidents_tool = mcp.tool(
7076
list_repo_incidents,
7177
description="List secret incidents or occurrences related to a specific repository, and assigned to the current user."
7278
"By default, this tool only shows incidents assigned to the current user. "
7379
"Only pass mine=False to get all incidents related to this repo if the user explicitly asks for all incidents even the ones not assigned to him.",
7480
required_scopes=["incidents:read", "sources:read"],
7581
)
82+
list_incidents_tool.parameters = compress_pydantic_model_schema(list_incidents_tool.parameters)
7683

77-
mcp.tool(
84+
list_occurrences_tool = mcp.tool(
7885
list_repo_occurrences,
7986
description="List secret occurrences for a specific repository with exact match locations. "
8087
"Returns detailed occurrence data including file paths, line numbers, and character indices where secrets were detected. "
8188
"Use this tool when you need to locate and remediate secrets in the codebase with precise file locations.",
8289
required_scopes=["incidents:read"],
8390
)
91+
list_occurrences_tool.parameters = compress_pydantic_model_schema(list_occurrences_tool.parameters)
8492

93+
# find_current_source_id doesn't use a Pydantic model parameter, so no compression needed
8594
mcp.tool(
8695
find_current_source_id,
8796
description="Find the GitGuardian source_id for a repository. "
@@ -91,20 +100,23 @@ def register_developer_tools(mcp: FastMCP):
91100
required_scopes=["sources:read"],
92101
)
93102

94-
mcp.tool(
103+
generate_token_tool = mcp.tool(
95104
generate_honeytoken,
96105
description="Generate an AWS GitGuardian honeytoken and get injection recommendations",
97106
required_scopes=["honeytokens:write"],
98107
)
108+
generate_token_tool.parameters = compress_pydantic_model_schema(generate_token_tool.parameters)
99109

100-
mcp.tool(
110+
list_tokens_tool = mcp.tool(
101111
list_honeytokens,
102112
description="List honeytokens from the GitGuardian dashboard with filtering options",
103113
required_scopes=["honeytokens:read"],
104114
)
115+
list_tokens_tool.parameters = compress_pydantic_model_schema(list_tokens_tool.parameters)
105116

106-
mcp.tool(
117+
list_users_tool = mcp.tool(
107118
list_users,
108119
description="List users on the workspace/account",
109120
required_scopes=["members:read"],
110121
)
122+
list_users_tool.parameters = compress_pydantic_model_schema(list_users_tool.parameters)

packages/gg_api_core/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies = [
2828
"python-dotenv>=1.0.0",
2929
"pydantic-settings>=2.0.0",
3030
"jinja2>=3.1.0",
31+
"jsonref>=1.1.0",
3132
]
3233
license = {text = "MIT"}
3334

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Utility functions for MCP tool schema manipulation."""
2+
3+
import json
4+
import logging
5+
6+
import jsonref
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def compress_pydantic_model_schema(tool_parameters: dict) -> dict:
12+
"""
13+
Compress a tool schema by flattening nested Pydantic model parameters.
14+
15+
When FastMCP tools use a single Pydantic model as a parameter (e.g., `params: MyParamsModel`),
16+
the generated schema nests all the model's fields under that parameter name. This creates
17+
an extra level of nesting that some MCP clients (like Claude Code) handle incorrectly.
18+
19+
This function flattens the schema by:
20+
1. Resolving all JSON references
21+
2. Extracting the properties from the nested model
22+
3. Promoting them to top-level parameters
23+
24+
Example:
25+
Before compression:
26+
{
27+
"type": "object",
28+
"properties": {
29+
"params": {
30+
"type": "object",
31+
"properties": {
32+
"source_id": {"type": "integer"},
33+
"get_all": {"type": "boolean"}
34+
}
35+
}
36+
}
37+
}
38+
39+
After compression:
40+
{
41+
"type": "object",
42+
"properties": {
43+
"source_id": {"type": "integer"},
44+
"get_all": {"type": "boolean"}
45+
}
46+
}
47+
48+
Args:
49+
tool_parameters: The tool's parameters schema (from tool.parameters)
50+
51+
Returns:
52+
Compressed schema with flattened parameters
53+
"""
54+
try:
55+
# Resolve all JSON references
56+
resolved = jsonref.replace_refs(tool_parameters)
57+
58+
# Convert back to plain dict (jsonref returns a special JsonRef object)
59+
compressed = json.loads(jsonref.dumps(resolved))
60+
61+
logger.debug(f"Compressed tool schema: {json.dumps(compressed, indent=2)}")
62+
return compressed
63+
except Exception as e:
64+
logger.exception(f"Failed to compress schema: {str(e)}")
65+
# Return original schema if compression fails
66+
return tool_parameters

packages/secops_mcp_server/src/secops_mcp_server/server.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from developer_mcp_server.register_tools import register_developer_tools
88
from fastmcp.exceptions import ToolError
99
from gg_api_core.mcp_server import get_mcp_server
10+
from gg_api_core.schema_utils import compress_pydantic_model_schema
1011
from gg_api_core.scopes import set_secops_scopes
1112
from gg_api_core.tools.assign_incident import assign_incident
1213
from gg_api_core.tools.create_code_fix_request import create_code_fix_request
@@ -149,59 +150,70 @@ async def get_current_token_info() -> dict[str, Any]:
149150
raise ToolError(f"Error: {str(e)}")
150151

151152

152-
mcp.tool(
153+
# Register SecOps tools with schema compression for Pydantic model parameters
154+
155+
update_tags_tool = mcp.tool(
153156
update_or_create_incident_custom_tags,
154157
description="Update or create custom tags for a secret incident",
155158
required_scopes=["incidents:write", "custom_tags:write"],
156159
)
160+
update_tags_tool.parameters = compress_pydantic_model_schema(update_tags_tool.parameters)
157161

158-
mcp.tool(
162+
update_status_tool = mcp.tool(
159163
update_incident_status,
160164
description="Update a secret incident with status",
161165
required_scopes=["incidents:write"],
162166
)
167+
update_status_tool.parameters = compress_pydantic_model_schema(update_status_tool.parameters)
163168

164-
mcp.tool(
169+
read_tags_tool = mcp.tool(
165170
read_custom_tags,
166171
description="Read custom tags from the GitGuardian dashboard.",
167172
required_scopes=["custom_tags:read"],
168173
)
174+
read_tags_tool.parameters = compress_pydantic_model_schema(read_tags_tool.parameters)
169175

170-
mcp.tool(
176+
write_tags_tool = mcp.tool(
171177
write_custom_tags,
172178
description="Create or delete custom tags in the GitGuardian dashboard.",
173179
required_scopes=["custom_tags:write"],
174180
)
181+
write_tags_tool.parameters = compress_pydantic_model_schema(write_tags_tool.parameters)
175182

176-
mcp.tool(
183+
manage_incident_tool = mcp.tool(
177184
manage_private_incident,
178185
description="Manage a secret incident (assign, unassign, resolve, ignore, reopen)",
179186
required_scopes=["incidents:write"],
180187
)
188+
manage_incident_tool.parameters = compress_pydantic_model_schema(manage_incident_tool.parameters)
181189

182-
mcp.tool(
190+
list_users_tool = mcp.tool(
183191
list_users,
184192
description="List users on the workspace/account",
185193
required_scopes=["members:read"],
186194
)
195+
list_users_tool.parameters = compress_pydantic_model_schema(list_users_tool.parameters)
187196

188-
mcp.tool(
197+
revoke_secret_tool = mcp.tool(
189198
revoke_secret,
190199
description="Revoke a secret by its ID through the GitGuardian API",
191200
required_scopes=["write:secret"],
192201
)
202+
revoke_secret_tool.parameters = compress_pydantic_model_schema(revoke_secret_tool.parameters)
193203

194-
mcp.tool(
204+
assign_incident_tool = mcp.tool(
195205
assign_incident,
196206
description="Assign a secret incident to a specific member or to the current user",
197207
required_scopes=["incidents:write"],
198208
)
209+
assign_incident_tool.parameters = compress_pydantic_model_schema(assign_incident_tool.parameters)
199210

200-
mcp.tool(
211+
create_fix_tool = mcp.tool(
201212
create_code_fix_request,
202213
description="Create code fix requests for multiple secret incidents with their locations. This will generate pull requests to automatically remediate the detected secrets.",
203214
required_scopes=["incidents:write"],
204215
)
216+
create_fix_tool.parameters = compress_pydantic_model_schema(create_fix_tool.parameters)
205217

206218
# Register common tools for user information and token management
207219
try:

0 commit comments

Comments
 (0)