claude-sdk-proxy 3.1.4 → 3.2.0
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.
- package/package.json +1 -1
- package/src/mcpTools.ts +183 -3
- package/src/proxy/server.ts +182 -265
- package/src/proxy/server.ts.bak +0 -1841
package/package.json
CHANGED
package/src/mcpTools.ts
CHANGED
|
@@ -1,3 +1,183 @@
|
|
|
1
|
-
// mcpTools.ts —
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// mcpTools.ts — Converts Anthropic API tool definitions to native MCP tools
|
|
2
|
+
// via the Claude Agent SDK's in-process MCP server support.
|
|
3
|
+
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"
|
|
6
|
+
|
|
7
|
+
// ── JSON Schema → Zod converter ─────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a JSON Schema object (as used in Anthropic tool input_schema)
|
|
11
|
+
* to a Zod schema. Handles: string, number, integer, boolean, array,
|
|
12
|
+
* object, enum, nullable, required/optional, nested objects, anyOf/oneOf.
|
|
13
|
+
* Falls back to z.any() for unrecognized types.
|
|
14
|
+
*/
|
|
15
|
+
export function jsonSchemaToZod(schema: any): z.ZodTypeAny {
|
|
16
|
+
if (!schema || typeof schema !== "object") return z.any()
|
|
17
|
+
|
|
18
|
+
// anyOf / oneOf → z.union
|
|
19
|
+
if (schema.anyOf || schema.oneOf) {
|
|
20
|
+
const variants = (schema.anyOf ?? schema.oneOf) as any[]
|
|
21
|
+
// Common pattern: { anyOf: [{ type: "string" }, { type: "null" }] } → nullable
|
|
22
|
+
const nonNull = variants.filter((v: any) => v.type !== "null")
|
|
23
|
+
const hasNull = variants.some((v: any) => v.type === "null")
|
|
24
|
+
if (hasNull && nonNull.length === 1) {
|
|
25
|
+
return jsonSchemaToZod(nonNull[0]).nullable()
|
|
26
|
+
}
|
|
27
|
+
if (variants.length === 0) return z.any()
|
|
28
|
+
if (variants.length === 1) return jsonSchemaToZod(variants[0])
|
|
29
|
+
const zodVariants = variants.map((v: any) => jsonSchemaToZod(v))
|
|
30
|
+
// z.union needs at least 2 elements
|
|
31
|
+
return z.union(zodVariants as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// enum → z.enum or z.union of literals
|
|
35
|
+
if (schema.enum) {
|
|
36
|
+
const values = schema.enum as any[]
|
|
37
|
+
if (values.length === 0) return z.any()
|
|
38
|
+
// String enums
|
|
39
|
+
if (values.every((v: any) => typeof v === "string")) {
|
|
40
|
+
return z.enum(values as [string, ...string[]])
|
|
41
|
+
}
|
|
42
|
+
// Mixed literal types
|
|
43
|
+
const literals = values.map((v: any) => z.literal(v))
|
|
44
|
+
if (literals.length === 1) return literals[0]!
|
|
45
|
+
return z.union(literals as unknown as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// const → z.literal
|
|
49
|
+
if ("const" in schema) {
|
|
50
|
+
return z.literal(schema.const)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (schema.type) {
|
|
54
|
+
case "string":
|
|
55
|
+
return z.string()
|
|
56
|
+
|
|
57
|
+
case "number":
|
|
58
|
+
case "integer":
|
|
59
|
+
return z.number()
|
|
60
|
+
|
|
61
|
+
case "boolean":
|
|
62
|
+
return z.boolean()
|
|
63
|
+
|
|
64
|
+
case "null":
|
|
65
|
+
return z.null()
|
|
66
|
+
|
|
67
|
+
case "array": {
|
|
68
|
+
const itemSchema = schema.items ? jsonSchemaToZod(schema.items) : z.any()
|
|
69
|
+
return z.array(itemSchema)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case "object": {
|
|
73
|
+
return jsonSchemaObjectToZod(schema)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
default: {
|
|
77
|
+
// Handle arrays of types like { type: ["string", "null"] }
|
|
78
|
+
if (Array.isArray(schema.type)) {
|
|
79
|
+
const types = schema.type as string[]
|
|
80
|
+
const hasNull = types.includes("null")
|
|
81
|
+
const nonNullTypes = types.filter((t: string) => t !== "null")
|
|
82
|
+
if (nonNullTypes.length === 0) return z.null()
|
|
83
|
+
if (nonNullTypes.length === 1) {
|
|
84
|
+
const base = jsonSchemaToZod({ ...schema, type: nonNullTypes[0] })
|
|
85
|
+
return hasNull ? base.nullable() : base
|
|
86
|
+
}
|
|
87
|
+
const variants = nonNullTypes.map((t: string) => jsonSchemaToZod({ ...schema, type: t }))
|
|
88
|
+
if (hasNull) variants.push(z.null())
|
|
89
|
+
if (variants.length === 1) return variants[0]!
|
|
90
|
+
return z.union(variants as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// No type specified but has properties → treat as object
|
|
94
|
+
if (schema.properties) {
|
|
95
|
+
return jsonSchemaObjectToZod(schema)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return z.any()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function jsonSchemaObjectToZod(schema: any): z.ZodTypeAny {
|
|
104
|
+
const properties = schema.properties ?? {}
|
|
105
|
+
const required = new Set<string>(schema.required ?? [])
|
|
106
|
+
const shape: Record<string, z.ZodTypeAny> = {}
|
|
107
|
+
|
|
108
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
109
|
+
let zodProp = jsonSchemaToZod(propSchema as any)
|
|
110
|
+
if (!required.has(key)) {
|
|
111
|
+
zodProp = zodProp.optional()
|
|
112
|
+
}
|
|
113
|
+
shape[key] = zodProp
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Object.keys(shape).length === 0) {
|
|
117
|
+
return z.object({}).passthrough()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use passthrough to allow additional properties (common in tool schemas)
|
|
121
|
+
return z.object(shape).passthrough()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Zod shape extraction ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extract the "raw shape" from a Zod object schema for use with the SDK's
|
|
128
|
+
* tool() helper which expects a ZodRawShape (Record<string, ZodTypeAny>).
|
|
129
|
+
* If the schema isn't a ZodObject, wrap it as { input: schema }.
|
|
130
|
+
*/
|
|
131
|
+
function extractZodShape(zodSchema: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
132
|
+
// ZodObject stores its shape
|
|
133
|
+
if (zodSchema instanceof z.ZodObject) {
|
|
134
|
+
return zodSchema.shape
|
|
135
|
+
}
|
|
136
|
+
// Fallback: wrap non-object schemas
|
|
137
|
+
return { input: zodSchema }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── MCP Server factory ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
interface AnthropicToolDef {
|
|
143
|
+
name: string
|
|
144
|
+
description?: string
|
|
145
|
+
input_schema?: any
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create an in-process MCP server from an array of Anthropic tool definitions.
|
|
150
|
+
* Each tool is registered with the SDK's tool() helper. The handlers are
|
|
151
|
+
* no-ops since canUseTool will deny execution (client handles it).
|
|
152
|
+
*/
|
|
153
|
+
export function createToolMcpServer(tools: AnthropicToolDef[]) {
|
|
154
|
+
const toolDefs = tools.map((t) => {
|
|
155
|
+
let zodSchema: z.ZodTypeAny
|
|
156
|
+
try {
|
|
157
|
+
zodSchema = jsonSchemaToZod(t.input_schema ?? { type: "object", properties: {} })
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// If conversion fails, use a passthrough object as fallback
|
|
160
|
+
zodSchema = z.object({}).passthrough()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const shape = extractZodShape(zodSchema)
|
|
164
|
+
|
|
165
|
+
return tool(
|
|
166
|
+
t.name,
|
|
167
|
+
t.description ?? "",
|
|
168
|
+
shape,
|
|
169
|
+
async () => {
|
|
170
|
+
// Should never be called — canUseTool denies all execution
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text" as const, text: "Error: tool execution denied by proxy" }],
|
|
173
|
+
isError: true,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
return createSdkMcpServer({
|
|
180
|
+
name: "proxy-tools",
|
|
181
|
+
tools: toolDefs,
|
|
182
|
+
})
|
|
183
|
+
}
|