or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration-options.mdcustom-tools.mderror-handling.mdhook-system.mdindex.mdinteractive-client.mdmessage-types.mdsimple-queries.mdtransport-system.md

custom-tools.mddocs/

0

# Custom Tools

1

2

Create custom tools that Claude can invoke using in-process MCP servers that run directly within your Python application. These provide better performance than external MCP servers, simpler deployment, and direct access to your application's state.

3

4

## Capabilities

5

6

### Tool Decorator

7

8

Decorator for defining MCP tools with type safety and automatic registration.

9

10

```python { .api }

11

def tool(

12

name: str, description: str, input_schema: type | dict[str, Any]

13

) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:

14

"""

15

Decorator for defining MCP tools with type safety.

16

17

Creates a tool that can be used with SDK MCP servers. The tool runs

18

in-process within your Python application, providing better performance

19

than external MCP servers.

20

21

Args:

22

name: Unique identifier for the tool. This is what Claude will use

23

to reference the tool in function calls.

24

description: Human-readable description of what the tool does.

25

This helps Claude understand when to use the tool.

26

input_schema: Schema defining the tool's input parameters.

27

Can be either:

28

- A dictionary mapping parameter names to types (e.g., {"text": str})

29

- A TypedDict class for more complex schemas

30

- A JSON Schema dictionary for full validation

31

32

Returns:

33

A decorator function that wraps the tool implementation and returns

34

an SdkMcpTool instance ready for use with create_sdk_mcp_server().

35

"""

36

```

37

38

### SDK MCP Tool

39

40

Definition structure for an SDK MCP tool containing metadata and handler function.

41

42

```python { .api }

43

@dataclass

44

class SdkMcpTool(Generic[T]):

45

"""Definition for an SDK MCP tool."""

46

47

name: str

48

description: str

49

input_schema: type[T] | dict[str, Any]

50

handler: Callable[[T], Awaitable[dict[str, Any]]]

51

```

52

53

### SDK MCP Server Creation

54

55

Create an in-process MCP server that runs within your Python application.

56

57

```python { .api }

58

def create_sdk_mcp_server(

59

name: str, version: str = "1.0.0", tools: list[SdkMcpTool[Any]] | None = None

60

) -> McpSdkServerConfig:

61

"""

62

Create an in-process MCP server that runs within your Python application.

63

64

Unlike external MCP servers that run as separate processes, SDK MCP servers

65

run directly in your application's process. This provides:

66

- Better performance (no IPC overhead)

67

- Simpler deployment (single process)

68

- Easier debugging (same process)

69

- Direct access to your application's state

70

71

Args:

72

name: Unique identifier for the server. This name is used to reference

73

the server in the mcp_servers configuration.

74

version: Server version string. Defaults to "1.0.0". This is for

75

informational purposes and doesn't affect functionality.

76

tools: List of SdkMcpTool instances created with the @tool decorator.

77

These are the functions that Claude can call through this server.

78

If None or empty, the server will have no tools (rarely useful).

79

80

Returns:

81

McpSdkServerConfig: A configuration object that can be passed to

82

ClaudeCodeOptions.mcp_servers. This config contains the server

83

instance and metadata needed for the SDK to route tool calls.

84

"""

85

```

86

87

## Usage Examples

88

89

### Basic Tool Creation

90

91

```python

92

from claude_code_sdk import tool, create_sdk_mcp_server, ClaudeCodeOptions, ClaudeSDKClient

93

94

@tool("greet", "Greet a user", {"name": str})

95

async def greet_user(args):

96

return {

97

"content": [

98

{"type": "text", "text": f"Hello, {args['name']}!"}

99

]

100

}

101

102

async def main():

103

# Create an SDK MCP server

104

server = create_sdk_mcp_server(

105

name="my-tools",

106

version="1.0.0",

107

tools=[greet_user]

108

)

109

110

# Use it with Claude

111

options = ClaudeCodeOptions(

112

mcp_servers={"tools": server},

113

allowed_tools=["mcp__tools__greet"] # Note the naming convention

114

)

115

116

async with ClaudeSDKClient(options=options) as client:

117

await client.query("Greet Alice")

118

119

async for msg in client.receive_response():

120

print(msg)

121

```

122

123

### Tool with Multiple Parameters

124

125

```python

126

@tool("add", "Add two numbers", {"a": float, "b": float})

127

async def add_numbers(args):

128

result = args["a"] + args["b"]

129

return {

130

"content": [

131

{"type": "text", "text": f"Result: {result}"}

132

]

133

}

134

135

@tool("multiply", "Multiply two numbers", {"a": float, "b": float})

136

async def multiply_numbers(args):

137

result = args["a"] * args["b"]

138

return {

139

"content": [

140

{"type": "text", "text": f"Product: {result}"}

141

]

142

}

143

144

async def main():

145

calculator = create_sdk_mcp_server(

146

name="calculator",

147

version="2.0.0",

148

tools=[add_numbers, multiply_numbers]

149

)

150

151

options = ClaudeCodeOptions(

152

mcp_servers={"calc": calculator},

153

allowed_tools=["mcp__calc__add", "mcp__calc__multiply"]

154

)

155

156

async with ClaudeSDKClient(options=options) as client:

157

await client.query("Calculate 15 + 27, then multiply the result by 3")

158

159

async for msg in client.receive_response():

160

print(msg)

161

```

162

163

### Tool with Error Handling

164

165

```python

166

@tool("divide", "Divide two numbers", {"a": float, "b": float})

167

async def divide_numbers(args):

168

if args["b"] == 0:

169

return {

170

"content": [

171

{"type": "text", "text": "Error: Division by zero"}

172

],

173

"is_error": True

174

}

175

176

result = args["a"] / args["b"]

177

return {

178

"content": [

179

{"type": "text", "text": f"Result: {result}"}

180

]

181

}

182

```

183

184

### Tool with Complex Schema

185

186

```python

187

from typing_extensions import TypedDict

188

189

class SearchParameters(TypedDict):

190

query: str

191

max_results: int

192

include_metadata: bool

193

194

@tool("search", "Search for items", SearchParameters)

195

async def search_items(args):

196

# Access typed parameters

197

query = args["query"]

198

max_results = args.get("max_results", 10)

199

include_metadata = args.get("include_metadata", False)

200

201

# Perform search logic

202

results = perform_search(query, max_results, include_metadata)

203

204

return {

205

"content": [

206

{"type": "text", "text": f"Found {len(results)} results for '{query}'"}

207

]

208

}

209

210

# Alternative: JSON Schema approach

211

json_schema = {

212

"type": "object",

213

"properties": {

214

"query": {"type": "string", "description": "Search query"},

215

"max_results": {"type": "integer", "minimum": 1, "maximum": 100, "default": 10},

216

"include_metadata": {"type": "boolean", "default": False}

217

},

218

"required": ["query"]

219

}

220

221

@tool("advanced_search", "Advanced search with JSON schema", json_schema)

222

async def advanced_search(args):

223

# Implementation

224

pass

225

```

226

227

### Tool with Application State Access

228

229

```python

230

class DataStore:

231

def __init__(self):

232

self.items = []

233

self.counter = 0

234

235

# Global application state

236

app_store = DataStore()

237

238

@tool("add_item", "Add item to store", {"item": str})

239

async def add_item(args):

240

app_store.items.append(args["item"])

241

app_store.counter += 1

242

return {

243

"content": [

244

{"type": "text", "text": f"Added: {args['item']} (total: {app_store.counter})"}

245

]

246

}

247

248

@tool("list_items", "List all items in store", {})

249

async def list_items(args):

250

if not app_store.items:

251

return {

252

"content": [

253

{"type": "text", "text": "No items in store"}

254

]

255

}

256

257

items_text = "\n".join(f"- {item}" for item in app_store.items)

258

return {

259

"content": [

260

{"type": "text", "text": f"Items in store:\n{items_text}"}

261

]

262

}

263

264

async def main():

265

server = create_sdk_mcp_server(

266

name="store",

267

tools=[add_item, list_items]

268

)

269

270

options = ClaudeCodeOptions(

271

mcp_servers={"store": server},

272

allowed_tools=["mcp__store__add_item", "mcp__store__list_items"]

273

)

274

275

async with ClaudeSDKClient(options=options) as client:

276

await client.query("Add 'apple' to the store, then list all items")

277

278

async for msg in client.receive_response():

279

print(msg)

280

```

281

282

### Mixed Server Configuration

283

284

You can use both SDK and external MCP servers together:

285

286

```python

287

# SDK server for in-process tools

288

internal_server = create_sdk_mcp_server(

289

name="internal",

290

tools=[my_custom_tool]

291

)

292

293

# External server configuration

294

external_server_config = {

295

"type": "stdio",

296

"command": "external-mcp-server",

297

"args": ["--config", "config.json"]

298

}

299

300

options = ClaudeCodeOptions(

301

mcp_servers={

302

"internal": internal_server, # In-process SDK server

303

"external": external_server_config # External subprocess server

304

},

305

allowed_tools=[

306

"mcp__internal__my_custom_tool",

307

"mcp__external__some_external_tool"

308

]

309

)

310

```

311

312

## Tool Naming Convention

313

314

When using SDK MCP servers, tools are referenced using the pattern:

315

`mcp__<server_name>__<tool_name>`

316

317

For example:

318

- Server named "calculator" with tool "add" → `mcp__calculator__add`

319

- Server named "my-tools" with tool "greet" → `mcp__my-tools__greet`

320

321

## Benefits Over External MCP Servers

322

323

**Performance:**

324

- No subprocess or IPC overhead

325

- Direct function calls within the same process

326

- Faster tool execution and response times

327

328

**Deployment:**

329

- Single Python process instead of multiple processes

330

- No need to manage external server lifecycles

331

- Simplified packaging and distribution

332

333

**Development:**

334

- Direct access to your application's variables and state

335

- Same debugging environment as your main application

336

- No cross-process communication complexity

337

- Standard Python exception handling

338

339

**Type Safety:**

340

- Direct Python function calls with type hints

341

- IDE support for tool function definitions

342

- Compile-time type checking

343

344

## Tool Function Requirements

345

346

All tool functions must:

347

348

1. **Be async**: Defined with `async def`

349

2. **Accept single dict argument**: Receive input parameters as a dictionary

350

3. **Return dict with content**: Return response in the expected format

351

4. **Handle errors appropriately**: Use `"is_error": True` for error responses

352

353

**Response Format:**

354

```python

355

{

356

"content": [

357

{"type": "text", "text": "Response text"}

358

],

359

"is_error": False # Optional, defaults to False

360

}

361

```

362

363

## Integration with Configuration

364

365

SDK MCP servers are configured through the `ClaudeCodeOptions.mcp_servers` field and work seamlessly with all other Claude Code SDK features including permission systems, hooks, and transport customization.

366

367

See [Configuration and Options](./configuration-options.md) for complete configuration details.