@musashishao/agent-kit 1.0.0 → 1.1.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.

Potentially problematic release.


This version of @musashishao/agent-kit might be problematic. Click here for more details.

@@ -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 |