Skip to content

Commit 30462f5

Browse files
authored
MCP: cleanup toolconverters (#229)
1 parent 4544287 commit 30462f5

File tree

3 files changed

+74
-205
lines changed

3 files changed

+74
-205
lines changed

go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ require (
99
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1
1010
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
1111
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1
12-
github.com/anthropics/anthropic-sdk-go v1.17.0
1312
github.com/coder/websocket v1.8.14
1413
github.com/google/uuid v1.6.0
1514
github.com/iancoleman/strcase v0.3.0
16-
github.com/openai/openai-go v0.1.0-alpha.61
15+
github.com/openai/openai-go/v2 v2.7.1
1716
github.com/shopspring/decimal v1.4.0
1817
github.com/stretchr/testify v1.10.0
1918
github.com/testcontainers/testcontainers-go v0.37.0

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3Xow
2424
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
2525
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
2626
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
27-
github.com/anthropics/anthropic-sdk-go v1.17.0 h1:BwK8ApcmaAUkvZTiQE0yi3R9XneEFskDIjLTmOAFZxQ=
28-
github.com/anthropics/anthropic-sdk-go v1.17.0/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
2927
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
3028
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
3129
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -109,8 +107,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
109107
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
110108
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
111109
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
112-
github.com/openai/openai-go v0.1.0-alpha.61 h1:dLJW1Dk15VAwm76xyPsiPt/Ky94NNGoMLETAI1ISoBY=
113-
github.com/openai/openai-go v0.1.0-alpha.61/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A=
110+
github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
111+
github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE=
114112
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
115113
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
116114
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=

pkg/toolconverters/mcp_converters.go

Lines changed: 71 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -25,43 +25,60 @@ package toolconverters
2525

2626
import (
2727
"encoding/json"
28+
"fmt"
2829

2930
"github.com/ansys/aali-sharedtypes/pkg/logging"
3031
"github.com/ansys/aali-sharedtypes/pkg/sharedtypes"
31-
"github.com/anthropics/anthropic-sdk-go"
32-
"github.com/anthropics/anthropic-sdk-go/packages/param"
33-
"github.com/openai/openai-go"
32+
"github.com/openai/openai-go/v2"
33+
"github.com/openai/openai-go/v2/shared"
3434
)
3535

3636
// ConvertMCPToOpenAIFormat converts MCP tools to OpenAI function calling format.
37+
//
38+
// Parameters:
39+
//
40+
// ctx: The logging context map.
41+
// mcpTools: Array of MCP tool definitions.
42+
//
43+
// Returns:
44+
//
45+
// []openai.ChatCompletionToolUnionParam: OpenAI formatted tools.
46+
// []error: List of errors for tools that were skipped during conversion.
3747
func ConvertMCPToOpenAIFormat(
3848
ctx *logging.ContextMap,
3949
mcpTools []interface{},
40-
) []openai.ChatCompletionToolParam {
41-
var openaiTools []openai.ChatCompletionToolParam
50+
) ([]openai.ChatCompletionToolUnionParam, []error) {
51+
var openaiTools []openai.ChatCompletionToolUnionParam
52+
var errors []error
4253

4354
for i, mcpTool := range mcpTools {
4455
// Convert interface{} to map for field access
4556
toolMap, ok := mcpTool.(map[string]interface{})
4657
if !ok {
47-
logging.Log.Warnf(ctx, "Skipping tool %d: not a valid object", i)
58+
toolJSON, _ := json.Marshal(mcpTool)
59+
err := fmt.Errorf("tool at index %d is not a valid object, got type %T, value: %s", i, mcpTool, string(toolJSON))
60+
errors = append(errors, err)
61+
logging.Log.Errorf(ctx, "Skipping tool %d: not a valid object (type: %T, value: %s)", i, mcpTool, string(toolJSON))
4862
continue
4963
}
5064

5165
// Extract required fields
5266
name, nameOk := toolMap["name"].(string)
5367
if !nameOk || name == "" {
54-
logging.Log.Warnf(ctx, "Skipping tool %d: missing or invalid 'name' field", i)
68+
toolJSON, _ := json.Marshal(toolMap)
69+
err := fmt.Errorf("tool at index %d is missing or has invalid 'name' field, tool data: %s", i, string(toolJSON))
70+
errors = append(errors, err)
71+
logging.Log.Errorf(ctx, "Skipping tool %d: missing or invalid 'name' field, tool data: %s", i, string(toolJSON))
5572
continue
5673
}
5774

58-
// Extract description (optional)
75+
// Extract description (
5976
description, _ := toolMap["description"].(string)
6077
if description == "" {
6178
logging.Log.Warnf(ctx, "Tool '%s': missing description (recommended for better LLM understanding)", name)
6279
}
6380

64-
// Extract inputSchema (optional)
81+
// Extract inputSchema
6582
inputSchema, schemaOk := toolMap["inputSchema"].(map[string]interface{})
6683
if !schemaOk {
6784
logging.Log.Warnf(ctx, "Tool '%s': missing or invalid 'inputSchema' (LLM may not understand parameters)", name)
@@ -72,218 +89,70 @@ func ConvertMCPToOpenAIFormat(
7289
}
7390
}
7491

75-
// Convert to OpenAI format using openai.F() wrappers
76-
openaiTool := openai.ChatCompletionToolParam{
77-
Type: openai.F(openai.ChatCompletionToolTypeFunction),
78-
Function: openai.F(openai.FunctionDefinitionParam{
79-
Name: openai.String(name),
80-
Description: openai.String(description),
81-
Parameters: openai.F(openai.FunctionParameters(inputSchema)),
82-
}),
92+
// Convert to OpenAI format
93+
functionDef := shared.FunctionDefinitionParam{
94+
Name: name,
95+
Description: openai.String(description),
96+
Parameters: shared.FunctionParameters(inputSchema),
8397
}
8498

99+
openaiTool := openai.ChatCompletionFunctionTool(functionDef)
85100
openaiTools = append(openaiTools, openaiTool)
86101
logging.Log.Debugf(ctx, "Converted MCP tool '%s' to OpenAI format", name)
87102
}
88103

89104
if len(openaiTools) > 0 {
90105
logging.Log.Infof(ctx, "Converted %d MCP tools to OpenAI format", len(openaiTools))
91-
} else if len(mcpTools) > 0 {
92-
logging.Log.Warnf(ctx, "No valid tools converted from %d MCP tools provided", len(mcpTools))
93106
}
94-
95-
return openaiTools
96-
}
97-
98-
// ConvertMCPToMistralFormat converts MCP tools to Mistral function calling format.
99-
func ConvertMCPToMistralFormat(
100-
ctx *logging.ContextMap,
101-
mcpTools []interface{},
102-
) []map[string]interface{} {
103-
var mistralTools []map[string]interface{}
104-
105-
for i, mcpTool := range mcpTools {
106-
// Convert interface{} to map for field access
107-
toolMap, ok := mcpTool.(map[string]interface{})
108-
if !ok {
109-
logging.Log.Warnf(ctx, "Skipping tool %d: not a valid object", i)
110-
continue
111-
}
112-
113-
// Extract required fields
114-
name, nameOk := toolMap["name"].(string)
115-
if !nameOk || name == "" {
116-
logging.Log.Warnf(ctx, "Skipping tool %d: missing or invalid 'name' field", i)
117-
continue
118-
}
119-
120-
// Extract description (optional)
121-
description, _ := toolMap["description"].(string)
122-
if description == "" {
123-
logging.Log.Warnf(ctx, "Tool '%s': missing description (recommended for better LLM understanding)", name)
124-
}
125-
126-
// Extract inputSchema (optional)
127-
inputSchema, schemaOk := toolMap["inputSchema"].(map[string]interface{})
128-
if !schemaOk {
129-
logging.Log.Warnf(ctx, "Tool '%s': missing or invalid 'inputSchema' (LLM may not understand parameters)", name)
130-
// Create empty schema as fallback
131-
inputSchema = map[string]interface{}{
132-
"type": "object",
133-
"properties": map[string]interface{}{},
134-
}
135-
}
136-
137-
// Convert to Mistral format (OpenAI-compatible)
138-
mistralTool := map[string]interface{}{
139-
"type": "function",
140-
"function": map[string]interface{}{
141-
"name": name,
142-
"description": description,
143-
"parameters": inputSchema,
144-
},
145-
}
146-
147-
mistralTools = append(mistralTools, mistralTool)
148-
logging.Log.Debugf(ctx, "Converted MCP tool '%s' to Mistral format", name)
149-
}
150-
151-
if len(mistralTools) > 0 {
152-
logging.Log.Infof(ctx, "Converted %d MCP tools to Mistral format", len(mistralTools))
153-
} else if len(mcpTools) > 0 {
154-
logging.Log.Warnf(ctx, "No valid tools converted from %d MCP tools provided", len(mcpTools))
107+
if len(errors) > 0 {
108+
logging.Log.Errorf(ctx, "Failed to convert %d out of %d MCP tools (see detailed errors above)", len(errors), len(mcpTools))
155109
}
156110

157-
return mistralTools
111+
return openaiTools, errors
158112
}
159113

160-
// ConvertMCPToAnthropicFormat converts MCP tools to Anthropic function calling format.
161-
func ConvertMCPToAnthropicFormat(
162-
ctx *logging.ContextMap,
163-
mcpTools []interface{},
164-
) []anthropic.ToolParam {
165-
var anthropicTools []anthropic.ToolParam
166-
167-
for i, mcpTool := range mcpTools {
168-
// Convert interface{} to map for field access
169-
toolMap, ok := mcpTool.(map[string]interface{})
170-
if !ok {
171-
logging.Log.Warnf(ctx, "Skipping tool %d: not a valid object", i)
172-
continue
173-
}
174-
175-
// Extract required fields
176-
name, nameOk := toolMap["name"].(string)
177-
if !nameOk || name == "" {
178-
logging.Log.Warnf(ctx, "Skipping tool %d: missing or invalid 'name' field", i)
179-
continue
180-
}
181-
182-
// Extract description (optional)
183-
description, _ := toolMap["description"].(string)
184-
if description == "" {
185-
logging.Log.Warnf(ctx, "Tool '%s': missing description (recommended for better LLM understanding)", name)
186-
}
187-
188-
// Extract inputSchema (optional)
189-
inputSchema, schemaOk := toolMap["inputSchema"].(map[string]interface{})
190-
if !schemaOk {
191-
logging.Log.Warnf(ctx, "Tool '%s': missing or invalid 'inputSchema' (LLM may not understand parameters)", name)
192-
// Create empty schema as fallback
193-
inputSchema = map[string]interface{}{
194-
"type": "object",
195-
"properties": map[string]interface{}{},
196-
}
197-
}
198-
199-
// Extract required fields from inputSchema
200-
var required []string
201-
if req, ok := inputSchema["required"].([]interface{}); ok {
202-
for _, r := range req {
203-
if rStr, ok := r.(string); ok {
204-
required = append(required, rStr)
205-
}
206-
}
207-
}
208-
209-
// Extract properties from inputSchema
210-
properties, _ := inputSchema["properties"]
211-
212-
// Convert to Anthropic format
213-
anthropicTool := anthropic.ToolParam{
214-
Name: name,
215-
InputSchema: anthropic.ToolInputSchemaParam{
216-
Type: "object",
217-
Properties: properties,
218-
Required: required,
219-
},
220-
}
221-
222-
// Add description if present
223-
if description != "" {
224-
anthropicTool.Description = param.NewOpt(description)
225-
}
226-
227-
anthropicTools = append(anthropicTools, anthropicTool)
228-
logging.Log.Debugf(ctx, "Converted MCP tool '%s' to Anthropic format", name)
229-
}
230-
231-
if len(anthropicTools) > 0 {
232-
logging.Log.Infof(ctx, "Converted %d MCP tools to Anthropic format", len(anthropicTools))
233-
} else if len(mcpTools) > 0 {
234-
logging.Log.Warnf(ctx, "No valid tools converted from %d MCP tools provided", len(mcpTools))
235-
}
236-
237-
return anthropicTools
238-
}
239-
240-
// ConvertAnthropicToolCallsToSharedTypes converts Anthropic ToolUseBlock responses to shared ToolCall format.
241-
func ConvertAnthropicToolCallsToSharedTypes(
242-
ctx *logging.ContextMap,
243-
toolUseBlocks []anthropic.ToolUseBlock,
244-
) []sharedtypes.ToolCall {
245-
var toolCalls []sharedtypes.ToolCall
246-
247-
for _, block := range toolUseBlocks {
248-
// Unmarshal json.RawMessage to map[string]interface{}
249-
var input map[string]interface{}
250-
if err := json.Unmarshal(block.Input, &input); err != nil {
251-
logging.Log.Warnf(ctx, "Failed to parse tool input for %s: %v", block.Name, err)
252-
input = make(map[string]interface{})
253-
}
254-
255-
toolCalls = append(toolCalls, sharedtypes.ToolCall{
256-
ID: block.ID,
257-
Type: "tool_use",
258-
Name: block.Name,
259-
Input: input,
260-
})
261-
}
262-
263-
if len(toolCalls) > 0 {
264-
logging.Log.Infof(ctx, "Converted %d Anthropic tool calls to shared format", len(toolCalls))
265-
}
266-
267-
return toolCalls
268-
}
269-
270-
// ConvertOpenAIToolCallsToSharedTypes converts OpenAI ChatCompletionMessageToolCall responses to shared ToolCall format.
114+
// ConvertOpenAIToolCallsToSharedTypes converts OpenAI SDK tool calls to shared ToolCall format.
115+
//
116+
// Parameters:
117+
//
118+
// ctx: The logging context map.
119+
// openaiToolCalls: Array of OpenAI tool call responses.
120+
//
121+
// Returns:
122+
//
123+
// []sharedtypes.ToolCall: Shared format tool calls.
124+
// []error: List of errors for tool calls that were skipped during conversion.
271125
func ConvertOpenAIToolCallsToSharedTypes(
272126
ctx *logging.ContextMap,
273-
openaiToolCalls []openai.ChatCompletionMessageToolCall,
274-
) []sharedtypes.ToolCall {
127+
openaiToolCalls []openai.ChatCompletionMessageToolCallUnion,
128+
) ([]sharedtypes.ToolCall, []error) {
275129
var toolCalls []sharedtypes.ToolCall
130+
var errors []error
131+
132+
for i, tc := range openaiToolCalls {
133+
// Skip tool calls with empty arguments
134+
if tc.Function.Arguments == "" {
135+
err := fmt.Errorf("tool call at index %d (ID: %s, Name: %s) has empty arguments", i, tc.ID, tc.Function.Name)
136+
errors = append(errors, err)
137+
logging.Log.Errorf(ctx, "Tool call at index %d (ID: %s, Name: %s) has empty arguments, skipping", i, tc.ID, tc.Function.Name)
138+
continue
139+
}
276140

277-
for _, tc := range openaiToolCalls {
141+
// Parse arguments
278142
var args map[string]interface{}
279143
if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
280-
logging.Log.Warnf(ctx, "Failed to parse tool call arguments for %s: %v", tc.Function.Name, err)
281-
args = make(map[string]interface{})
144+
parseErr := fmt.Errorf("failed to parse tool call at index %d (ID: %s, Name: %s): %w, raw arguments: %s",
145+
i, tc.ID, tc.Function.Name, err, tc.Function.Arguments)
146+
errors = append(errors, parseErr)
147+
logging.Log.Errorf(ctx, "Failed to parse tool call at index %d (ID: %s, Name: %s): %v, raw arguments: %s, skipping tool call",
148+
i, tc.ID, tc.Function.Name, err, tc.Function.Arguments)
149+
continue
282150
}
283151

152+
// Only append valid tool calls
284153
toolCalls = append(toolCalls, sharedtypes.ToolCall{
285154
ID: tc.ID,
286-
Type: "function",
155+
Type: string(tc.Type),
287156
Name: tc.Function.Name,
288157
Input: args,
289158
})
@@ -292,6 +161,9 @@ func ConvertOpenAIToolCallsToSharedTypes(
292161
if len(toolCalls) > 0 {
293162
logging.Log.Infof(ctx, "Converted %d OpenAI tool calls to shared format", len(toolCalls))
294163
}
164+
if len(errors) > 0 {
165+
logging.Log.Errorf(ctx, "Failed to convert %d out of %d tool calls (see detailed errors above)", len(errors), len(openaiToolCalls))
166+
}
295167

296-
return toolCalls
168+
return toolCalls, errors
297169
}

0 commit comments

Comments
 (0)