@musashishao/agent-kit 1.0.0 → 1.0.1
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.
Potentially problematic release.
This version of @musashishao/agent-kit might be problematic. Click here for more details.
- package/.agent/ARCHITECTURE.md +4 -2
- package/.agent/skills/mcp-builder/SKILL.md +583 -97
- package/.agent/skills/mcp-builder/python-template.md +522 -0
- package/.agent/skills/mcp-builder/tool-patterns.md +642 -0
- package/.agent/skills/mcp-builder/typescript-template.md +361 -0
- package/.agent/skills/problem-solving/SKILL.md +556 -0
- package/.agent/skills/problem-solving/collision-zone-thinking.md +285 -0
- package/.agent/skills/problem-solving/inversion-exercise.md +205 -0
- package/.agent/skills/problem-solving/meta-pattern-recognition.md +313 -0
- package/.agent/skills/problem-solving/scale-game.md +300 -0
- package/.agent/skills/problem-solving/simplification-cascades.md +321 -0
- package/.agent/skills/problem-solving/when-stuck.md +146 -0
- package/package.json +2 -2
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# MCP Tool Design Patterns
|
|
2
|
+
|
|
3
|
+
> Patterns for designing effective, user-friendly MCP tools.
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
| Principle | Description |
|
|
8
|
+
|-----------|-------------|
|
|
9
|
+
| **Single Purpose** | One tool does one thing well |
|
|
10
|
+
| **Clear Naming** | Action-oriented: verb_noun |
|
|
11
|
+
| **Validated Input** | Schema with types and descriptions |
|
|
12
|
+
| **Structured Output** | Predictable, parseable responses |
|
|
13
|
+
| **Graceful Errors** | Helpful error messages |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Pattern 1: CRUD Operations
|
|
18
|
+
|
|
19
|
+
For managing entities (users, tasks, documents):
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# CREATE
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
def create_task(
|
|
25
|
+
title: str,
|
|
26
|
+
description: str = "",
|
|
27
|
+
priority: str = "medium",
|
|
28
|
+
due_date: str = None
|
|
29
|
+
) -> dict:
|
|
30
|
+
"""Create a new task.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
title: Task title (required)
|
|
34
|
+
description: Detailed description
|
|
35
|
+
priority: One of 'low', 'medium', 'high'
|
|
36
|
+
due_date: Due date in YYYY-MM-DD format
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Created task with ID and timestamps
|
|
40
|
+
"""
|
|
41
|
+
task = {
|
|
42
|
+
"id": generate_id(),
|
|
43
|
+
"title": title,
|
|
44
|
+
"description": description,
|
|
45
|
+
"priority": priority,
|
|
46
|
+
"due_date": due_date,
|
|
47
|
+
"status": "pending",
|
|
48
|
+
"created_at": datetime.now().isoformat(),
|
|
49
|
+
}
|
|
50
|
+
save_task(task)
|
|
51
|
+
return {"success": True, "task": task}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# READ (single)
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def get_task(task_id: str) -> dict:
|
|
57
|
+
"""Get a task by ID.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
task_id: Unique task identifier
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Task details or error if not found
|
|
64
|
+
"""
|
|
65
|
+
task = load_task(task_id)
|
|
66
|
+
if not task:
|
|
67
|
+
return {"error": f"Task not found: {task_id}"}
|
|
68
|
+
return task
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# READ (list with filters)
|
|
72
|
+
@mcp.tool()
|
|
73
|
+
def list_tasks(
|
|
74
|
+
status: str = None,
|
|
75
|
+
priority: str = None,
|
|
76
|
+
limit: int = 10
|
|
77
|
+
) -> dict:
|
|
78
|
+
"""List tasks with optional filters.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
status: Filter by status ('pending', 'in_progress', 'done')
|
|
82
|
+
priority: Filter by priority ('low', 'medium', 'high')
|
|
83
|
+
limit: Maximum number of tasks to return
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of matching tasks
|
|
87
|
+
"""
|
|
88
|
+
tasks = load_all_tasks()
|
|
89
|
+
|
|
90
|
+
if status:
|
|
91
|
+
tasks = [t for t in tasks if t["status"] == status]
|
|
92
|
+
if priority:
|
|
93
|
+
tasks = [t for t in tasks if t["priority"] == priority]
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
"tasks": tasks[:limit],
|
|
97
|
+
"total": len(tasks),
|
|
98
|
+
"showing": min(limit, len(tasks))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# UPDATE
|
|
103
|
+
@mcp.tool()
|
|
104
|
+
def update_task(
|
|
105
|
+
task_id: str,
|
|
106
|
+
title: str = None,
|
|
107
|
+
status: str = None,
|
|
108
|
+
priority: str = None
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""Update task properties.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
task_id: Task to update
|
|
114
|
+
title: New title (optional)
|
|
115
|
+
status: New status (optional)
|
|
116
|
+
priority: New priority (optional)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Updated task or error
|
|
120
|
+
"""
|
|
121
|
+
task = load_task(task_id)
|
|
122
|
+
if not task:
|
|
123
|
+
return {"error": f"Task not found: {task_id}"}
|
|
124
|
+
|
|
125
|
+
if title:
|
|
126
|
+
task["title"] = title
|
|
127
|
+
if status:
|
|
128
|
+
task["status"] = status
|
|
129
|
+
if priority:
|
|
130
|
+
task["priority"] = priority
|
|
131
|
+
|
|
132
|
+
task["updated_at"] = datetime.now().isoformat()
|
|
133
|
+
save_task(task)
|
|
134
|
+
|
|
135
|
+
return {"success": True, "task": task}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# DELETE
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
def delete_task(task_id: str) -> dict:
|
|
141
|
+
"""Delete a task.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
task_id: Task to delete
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Confirmation or error
|
|
148
|
+
"""
|
|
149
|
+
task = load_task(task_id)
|
|
150
|
+
if not task:
|
|
151
|
+
return {"error": f"Task not found: {task_id}"}
|
|
152
|
+
|
|
153
|
+
remove_task(task_id)
|
|
154
|
+
return {"success": True, "deleted": task_id}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Pattern 2: API Integration
|
|
160
|
+
|
|
161
|
+
For connecting to external APIs:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import httpx
|
|
165
|
+
import os
|
|
166
|
+
|
|
167
|
+
@mcp.tool()
|
|
168
|
+
async def github_create_issue(
|
|
169
|
+
repo: str,
|
|
170
|
+
title: str,
|
|
171
|
+
body: str = "",
|
|
172
|
+
labels: list[str] = []
|
|
173
|
+
) -> dict:
|
|
174
|
+
"""Create a GitHub issue.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
repo: Repository in format 'owner/repo'
|
|
178
|
+
title: Issue title
|
|
179
|
+
body: Issue body in markdown
|
|
180
|
+
labels: List of label names to apply
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Created issue details with URL
|
|
184
|
+
"""
|
|
185
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
186
|
+
if not token:
|
|
187
|
+
return {"error": "GITHUB_TOKEN environment variable not set"}
|
|
188
|
+
|
|
189
|
+
async with httpx.AsyncClient() as client:
|
|
190
|
+
response = await client.post(
|
|
191
|
+
f"https://api.github.com/repos/{repo}/issues",
|
|
192
|
+
headers={
|
|
193
|
+
"Authorization": f"Bearer {token}",
|
|
194
|
+
"Accept": "application/vnd.github.v3+json",
|
|
195
|
+
},
|
|
196
|
+
json={
|
|
197
|
+
"title": title,
|
|
198
|
+
"body": body,
|
|
199
|
+
"labels": labels,
|
|
200
|
+
},
|
|
201
|
+
timeout=30.0,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if response.status_code == 201:
|
|
205
|
+
issue = response.json()
|
|
206
|
+
return {
|
|
207
|
+
"success": True,
|
|
208
|
+
"issue_number": issue["number"],
|
|
209
|
+
"url": issue["html_url"],
|
|
210
|
+
"title": issue["title"],
|
|
211
|
+
}
|
|
212
|
+
elif response.status_code == 401:
|
|
213
|
+
return {"error": "Invalid GitHub token"}
|
|
214
|
+
elif response.status_code == 404:
|
|
215
|
+
return {"error": f"Repository not found: {repo}"}
|
|
216
|
+
else:
|
|
217
|
+
return {
|
|
218
|
+
"error": f"GitHub API error: {response.status_code}",
|
|
219
|
+
"details": response.text[:200]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@mcp.tool()
|
|
224
|
+
async def fetch_url(
|
|
225
|
+
url: str,
|
|
226
|
+
method: str = "GET",
|
|
227
|
+
headers: dict = None
|
|
228
|
+
) -> dict:
|
|
229
|
+
"""Fetch content from a URL.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
url: URL to fetch
|
|
233
|
+
method: HTTP method (GET, POST, etc.)
|
|
234
|
+
headers: Optional headers to include
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Response content and metadata
|
|
238
|
+
"""
|
|
239
|
+
async with httpx.AsyncClient() as client:
|
|
240
|
+
try:
|
|
241
|
+
response = await client.request(
|
|
242
|
+
method=method,
|
|
243
|
+
url=url,
|
|
244
|
+
headers=headers or {},
|
|
245
|
+
timeout=30.0,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"status_code": response.status_code,
|
|
250
|
+
"content_type": response.headers.get("content-type", "unknown"),
|
|
251
|
+
"content": response.text[:10000], # Limit content size
|
|
252
|
+
"truncated": len(response.text) > 10000,
|
|
253
|
+
}
|
|
254
|
+
except httpx.ConnectError:
|
|
255
|
+
return {"error": f"Failed to connect to {url}"}
|
|
256
|
+
except httpx.TimeoutException:
|
|
257
|
+
return {"error": f"Request to {url} timed out"}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Pattern 3: File Operations
|
|
263
|
+
|
|
264
|
+
For reading/writing files safely:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from pathlib import Path
|
|
268
|
+
import mimetypes
|
|
269
|
+
|
|
270
|
+
# Define allowed directory
|
|
271
|
+
ALLOWED_DIR = Path.home() / "mcp-files"
|
|
272
|
+
|
|
273
|
+
def validate_path(path: str) -> Path:
|
|
274
|
+
"""Validate path is within allowed directory."""
|
|
275
|
+
resolved = Path(path).resolve()
|
|
276
|
+
|
|
277
|
+
if not resolved.is_relative_to(ALLOWED_DIR):
|
|
278
|
+
raise ValueError(f"Access denied: {path}")
|
|
279
|
+
|
|
280
|
+
return resolved
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@mcp.tool()
|
|
284
|
+
def read_file(path: str) -> dict:
|
|
285
|
+
"""Read a file's contents.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
path: Path relative to allowed directory
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
File contents and metadata
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
file_path = validate_path(path)
|
|
295
|
+
except ValueError as e:
|
|
296
|
+
return {"error": str(e)}
|
|
297
|
+
|
|
298
|
+
if not file_path.exists():
|
|
299
|
+
return {"error": f"File not found: {path}"}
|
|
300
|
+
|
|
301
|
+
if not file_path.is_file():
|
|
302
|
+
return {"error": f"Not a file: {path}"}
|
|
303
|
+
|
|
304
|
+
# Check file size
|
|
305
|
+
size = file_path.stat().st_size
|
|
306
|
+
if size > 1_000_000: # 1MB limit
|
|
307
|
+
return {"error": f"File too large: {size} bytes (max 1MB)"}
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
content = file_path.read_text()
|
|
311
|
+
except UnicodeDecodeError:
|
|
312
|
+
return {"error": "Cannot read binary file as text"}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
"path": str(file_path),
|
|
316
|
+
"content": content,
|
|
317
|
+
"size": size,
|
|
318
|
+
"mime_type": mimetypes.guess_type(str(file_path))[0],
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@mcp.tool()
|
|
323
|
+
def write_file(
|
|
324
|
+
path: str,
|
|
325
|
+
content: str,
|
|
326
|
+
append: bool = False
|
|
327
|
+
) -> dict:
|
|
328
|
+
"""Write content to a file.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
path: Path relative to allowed directory
|
|
332
|
+
content: Content to write
|
|
333
|
+
append: If true, append instead of overwrite
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Confirmation with file details
|
|
337
|
+
"""
|
|
338
|
+
try:
|
|
339
|
+
file_path = validate_path(path)
|
|
340
|
+
except ValueError as e:
|
|
341
|
+
return {"error": str(e)}
|
|
342
|
+
|
|
343
|
+
# Create parent directories
|
|
344
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
345
|
+
|
|
346
|
+
# Write content
|
|
347
|
+
mode = "a" if append else "w"
|
|
348
|
+
with open(file_path, mode) as f:
|
|
349
|
+
f.write(content)
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
"success": True,
|
|
353
|
+
"path": str(file_path),
|
|
354
|
+
"size": file_path.stat().st_size,
|
|
355
|
+
"mode": "appended" if append else "written",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@mcp.tool()
|
|
360
|
+
def list_directory(path: str = ".") -> dict:
|
|
361
|
+
"""List contents of a directory.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
path: Directory path relative to allowed directory
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
List of files and subdirectories
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
dir_path = validate_path(path)
|
|
371
|
+
except ValueError as e:
|
|
372
|
+
return {"error": str(e)}
|
|
373
|
+
|
|
374
|
+
if not dir_path.exists():
|
|
375
|
+
return {"error": f"Directory not found: {path}"}
|
|
376
|
+
|
|
377
|
+
if not dir_path.is_dir():
|
|
378
|
+
return {"error": f"Not a directory: {path}"}
|
|
379
|
+
|
|
380
|
+
items = []
|
|
381
|
+
for item in dir_path.iterdir():
|
|
382
|
+
items.append({
|
|
383
|
+
"name": item.name,
|
|
384
|
+
"type": "directory" if item.is_dir() else "file",
|
|
385
|
+
"size": item.stat().st_size if item.is_file() else None,
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
"path": str(dir_path),
|
|
390
|
+
"items": sorted(items, key=lambda x: (x["type"], x["name"])),
|
|
391
|
+
"count": len(items),
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Pattern 4: Database Queries
|
|
398
|
+
|
|
399
|
+
For safe database access:
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
import sqlite3
|
|
403
|
+
from typing import List, Any
|
|
404
|
+
|
|
405
|
+
def get_db_connection():
|
|
406
|
+
"""Get database connection."""
|
|
407
|
+
return sqlite3.connect(os.environ.get("DATABASE_PATH", "data.db"))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@mcp.tool()
|
|
411
|
+
def query_database(
|
|
412
|
+
sql: str,
|
|
413
|
+
params: List[Any] = []
|
|
414
|
+
) -> dict:
|
|
415
|
+
"""Execute a read-only SQL query.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
sql: SQL SELECT query
|
|
419
|
+
params: Query parameters for safe binding
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Query results as list of dictionaries
|
|
423
|
+
"""
|
|
424
|
+
# Validate read-only query
|
|
425
|
+
normalized = sql.strip().upper()
|
|
426
|
+
if not normalized.startswith("SELECT"):
|
|
427
|
+
return {"error": "Only SELECT queries are allowed"}
|
|
428
|
+
|
|
429
|
+
# Check for dangerous patterns
|
|
430
|
+
dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "CREATE", "--", ";"]
|
|
431
|
+
for pattern in dangerous:
|
|
432
|
+
if pattern in normalized:
|
|
433
|
+
return {"error": f"Query contains forbidden pattern: {pattern}"}
|
|
434
|
+
|
|
435
|
+
conn = get_db_connection()
|
|
436
|
+
conn.row_factory = sqlite3.Row
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
cursor = conn.execute(sql, params)
|
|
440
|
+
rows = [dict(row) for row in cursor.fetchall()]
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
"success": True,
|
|
444
|
+
"rows": rows,
|
|
445
|
+
"count": len(rows),
|
|
446
|
+
"columns": list(rows[0].keys()) if rows else [],
|
|
447
|
+
}
|
|
448
|
+
except sqlite3.Error as e:
|
|
449
|
+
return {"error": f"Database error: {str(e)}"}
|
|
450
|
+
finally:
|
|
451
|
+
conn.close()
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
@mcp.tool()
|
|
455
|
+
def get_table_schema(table: str) -> dict:
|
|
456
|
+
"""Get schema information for a table.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
table: Table name
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Column definitions and indexes
|
|
463
|
+
"""
|
|
464
|
+
# Validate table name (alphanumeric only)
|
|
465
|
+
if not table.replace("_", "").isalnum():
|
|
466
|
+
return {"error": "Invalid table name"}
|
|
467
|
+
|
|
468
|
+
conn = get_db_connection()
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
# Get column info
|
|
472
|
+
cursor = conn.execute(f"PRAGMA table_info({table})")
|
|
473
|
+
columns = [
|
|
474
|
+
{
|
|
475
|
+
"name": row[1],
|
|
476
|
+
"type": row[2],
|
|
477
|
+
"nullable": not row[3],
|
|
478
|
+
"primary_key": bool(row[5]),
|
|
479
|
+
}
|
|
480
|
+
for row in cursor.fetchall()
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
if not columns:
|
|
484
|
+
return {"error": f"Table not found: {table}"}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
"table": table,
|
|
488
|
+
"columns": columns,
|
|
489
|
+
}
|
|
490
|
+
finally:
|
|
491
|
+
conn.close()
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Pattern 5: Long-running Operations
|
|
497
|
+
|
|
498
|
+
For operations that take time:
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
import asyncio
|
|
502
|
+
from typing import Callable
|
|
503
|
+
|
|
504
|
+
@mcp.tool()
|
|
505
|
+
async def process_batch(
|
|
506
|
+
items: List[str],
|
|
507
|
+
operation: str = "process"
|
|
508
|
+
) -> dict:
|
|
509
|
+
"""Process a batch of items with progress tracking.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
items: List of items to process
|
|
513
|
+
operation: Operation to perform on each item
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
Results for all items
|
|
517
|
+
"""
|
|
518
|
+
results = []
|
|
519
|
+
errors = []
|
|
520
|
+
|
|
521
|
+
for i, item in enumerate(items):
|
|
522
|
+
try:
|
|
523
|
+
# Process item
|
|
524
|
+
result = await process_single_item(item, operation)
|
|
525
|
+
results.append({"item": item, "result": result})
|
|
526
|
+
|
|
527
|
+
# Log progress for debugging
|
|
528
|
+
progress = (i + 1) / len(items) * 100
|
|
529
|
+
logger.info(f"Progress: {progress:.0f}% ({i+1}/{len(items)})")
|
|
530
|
+
|
|
531
|
+
except Exception as e:
|
|
532
|
+
errors.append({"item": item, "error": str(e)})
|
|
533
|
+
|
|
534
|
+
# Avoid overwhelming the system
|
|
535
|
+
await asyncio.sleep(0.1)
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"total": len(items),
|
|
539
|
+
"successful": len(results),
|
|
540
|
+
"failed": len(errors),
|
|
541
|
+
"results": results,
|
|
542
|
+
"errors": errors,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
async def process_single_item(item: str, operation: str) -> str:
|
|
547
|
+
"""Process a single item."""
|
|
548
|
+
await asyncio.sleep(0.5) # Simulate work
|
|
549
|
+
return f"Processed {item} with {operation}"
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Error Handling Pattern
|
|
555
|
+
|
|
556
|
+
Consistent error responses:
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
from enum import Enum
|
|
560
|
+
from typing import Union
|
|
561
|
+
|
|
562
|
+
class ErrorCode(Enum):
|
|
563
|
+
NOT_FOUND = "not_found"
|
|
564
|
+
INVALID_INPUT = "invalid_input"
|
|
565
|
+
UNAUTHORIZED = "unauthorized"
|
|
566
|
+
RATE_LIMITED = "rate_limited"
|
|
567
|
+
INTERNAL_ERROR = "internal_error"
|
|
568
|
+
|
|
569
|
+
def error_response(
|
|
570
|
+
code: ErrorCode,
|
|
571
|
+
message: str,
|
|
572
|
+
details: dict = None
|
|
573
|
+
) -> dict:
|
|
574
|
+
"""Create a standardized error response."""
|
|
575
|
+
response = {
|
|
576
|
+
"success": False,
|
|
577
|
+
"error": {
|
|
578
|
+
"code": code.value,
|
|
579
|
+
"message": message,
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if details:
|
|
583
|
+
response["error"]["details"] = details
|
|
584
|
+
return response
|
|
585
|
+
|
|
586
|
+
def success_response(data: Union[dict, list]) -> dict:
|
|
587
|
+
"""Create a standardized success response."""
|
|
588
|
+
return {
|
|
589
|
+
"success": True,
|
|
590
|
+
"data": data,
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
# Usage
|
|
594
|
+
@mcp.tool()
|
|
595
|
+
def example_tool(id: str) -> dict:
|
|
596
|
+
"""Example with standardized responses."""
|
|
597
|
+
|
|
598
|
+
if not id:
|
|
599
|
+
return error_response(
|
|
600
|
+
ErrorCode.INVALID_INPUT,
|
|
601
|
+
"ID is required"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
item = load_item(id)
|
|
605
|
+
|
|
606
|
+
if not item:
|
|
607
|
+
return error_response(
|
|
608
|
+
ErrorCode.NOT_FOUND,
|
|
609
|
+
f"Item not found: {id}"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
return success_response(item)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Best Practices Checklist
|
|
618
|
+
|
|
619
|
+
### Before Publishing Your Tool
|
|
620
|
+
|
|
621
|
+
- [ ] **Naming**: Verb_noun format (create_task, get_user)
|
|
622
|
+
- [ ] **Description**: Clear, human-readable purpose
|
|
623
|
+
- [ ] **Args**: All parameters have descriptions
|
|
624
|
+
- [ ] **Validation**: Input is validated before processing
|
|
625
|
+
- [ ] **Errors**: All error paths return helpful messages
|
|
626
|
+
- [ ] **Output**: Structured JSON, consistent format
|
|
627
|
+
- [ ] **Security**: Sensitive data not logged
|
|
628
|
+
- [ ] **Limits**: Resource limits in place (file size, query results)
|
|
629
|
+
- [ ] **Testing**: Unit tests for critical paths
|
|
630
|
+
- [ ] **Documentation**: README explains usage
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Tool Naming Guidelines
|
|
635
|
+
|
|
636
|
+
| ❌ Bad | ✅ Good | Why |
|
|
637
|
+
|--------|---------|-----|
|
|
638
|
+
| `do_thing` | `create_user` | Specific action + entity |
|
|
639
|
+
| `process` | `parse_csv_file` | Clear what it does |
|
|
640
|
+
| `handle_data` | `validate_email` | Single purpose |
|
|
641
|
+
| `run` | `execute_query` | Descriptive |
|
|
642
|
+
| `getData` | `get_user_profile` | snake_case, explicit |
|