omgkit 2.0.7 → 2.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.
@@ -0,0 +1,883 @@
1
+ ---
2
+ name: mcp-development
3
+ description: Create Model Context Protocol servers, tools, and resources for AI assistant integration
4
+ category: tools
5
+ triggers:
6
+ - mcp server
7
+ - model context protocol
8
+ - claude tools
9
+ - ai tools
10
+ - mcp development
11
+ - fastmcp
12
+ - tool creation
13
+ ---
14
+
15
+ # MCP Development
16
+
17
+ Create **Model Context Protocol (MCP) servers** that extend AI assistants like Claude with custom tools, resources, and prompts. This skill covers server architecture, tool design, and production deployment patterns.
18
+
19
+ ## Purpose
20
+
21
+ MCP enables AI assistants to interact with external systems securely and efficiently:
22
+
23
+ - Create custom tools that AI can use during conversations
24
+ - Expose data resources for AI to read and analyze
25
+ - Define prompt templates for consistent AI interactions
26
+ - Build integrations with databases, APIs, and services
27
+ - Manage server lifecycle and error handling
28
+
29
+ ## Features
30
+
31
+ ### 1. FastMCP Server (Python)
32
+
33
+ ```python
34
+ # Basic MCP server with FastMCP
35
+ from fastmcp import FastMCP
36
+
37
+ # Initialize server
38
+ mcp = FastMCP("my-service")
39
+
40
+ # Define a simple tool
41
+ @mcp.tool()
42
+ def get_weather(city: str) -> str:
43
+ """Get current weather for a city.
44
+
45
+ Args:
46
+ city: Name of the city to get weather for
47
+
48
+ Returns:
49
+ Weather information as a formatted string
50
+ """
51
+ # Fetch from weather API
52
+ response = requests.get(f"https://api.weather.com/{city}")
53
+ data = response.json()
54
+
55
+ return f"""
56
+ Weather in {city}:
57
+ Temperature: {data['temp']}°F
58
+ Conditions: {data['conditions']}
59
+ Humidity: {data['humidity']}%
60
+ """
61
+
62
+ # Tool with complex parameters
63
+ @mcp.tool()
64
+ def search_database(
65
+ query: str,
66
+ table: str = "users",
67
+ limit: int = 10,
68
+ include_deleted: bool = False
69
+ ) -> list[dict]:
70
+ """Search the database with flexible parameters.
71
+
72
+ Args:
73
+ query: Search query string
74
+ table: Table to search (default: users)
75
+ limit: Maximum results to return (default: 10)
76
+ include_deleted: Include soft-deleted records
77
+
78
+ Returns:
79
+ List of matching records
80
+ """
81
+ sql = f"SELECT * FROM {table} WHERE content LIKE %s"
82
+ if not include_deleted:
83
+ sql += " AND deleted_at IS NULL"
84
+ sql += f" LIMIT {limit}"
85
+
86
+ return db.execute(sql, [f"%{query}%"]).fetchall()
87
+
88
+ # Resource exposure
89
+ @mcp.resource("config://settings")
90
+ def get_settings() -> str:
91
+ """Expose application settings as a resource."""
92
+ return json.dumps(load_settings(), indent=2)
93
+
94
+ # Dynamic resource with URI template
95
+ @mcp.resource("user://{user_id}/profile")
96
+ def get_user_profile(user_id: str) -> str:
97
+ """Get user profile data.
98
+
99
+ Args:
100
+ user_id: The user's unique identifier
101
+ """
102
+ user = db.get_user(user_id)
103
+ return json.dumps(user.to_dict())
104
+
105
+ # Prompt template
106
+ @mcp.prompt()
107
+ def analyze_code(code: str, language: str = "python") -> str:
108
+ """Generate a prompt for code analysis.
109
+
110
+ Args:
111
+ code: The code to analyze
112
+ language: Programming language
113
+ """
114
+ return f"""
115
+ Please analyze the following {language} code:
116
+
117
+ ```{language}
118
+ {code}
119
+ ```
120
+
121
+ Provide:
122
+ 1. A brief summary of what the code does
123
+ 2. Potential bugs or issues
124
+ 3. Performance considerations
125
+ 4. Security concerns
126
+ 5. Suggested improvements
127
+ """
128
+
129
+ # Run server
130
+ if __name__ == "__main__":
131
+ mcp.run()
132
+ ```
133
+
134
+ ### 2. TypeScript MCP Server
135
+
136
+ ```typescript
137
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
138
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
139
+ import {
140
+ CallToolRequestSchema,
141
+ ListResourcesRequestSchema,
142
+ ListToolsRequestSchema,
143
+ ReadResourceRequestSchema,
144
+ } from "@modelcontextprotocol/sdk/types.js";
145
+ import { z } from "zod";
146
+
147
+ // Server configuration
148
+ const server = new Server(
149
+ {
150
+ name: "my-mcp-server",
151
+ version: "1.0.0",
152
+ },
153
+ {
154
+ capabilities: {
155
+ tools: {},
156
+ resources: {},
157
+ prompts: {},
158
+ },
159
+ }
160
+ );
161
+
162
+ // Tool definitions with Zod schemas
163
+ const FileSearchSchema = z.object({
164
+ query: z.string().describe("Search query"),
165
+ directory: z.string().optional().describe("Directory to search in"),
166
+ fileTypes: z.array(z.string()).optional().describe("File extensions to include"),
167
+ maxResults: z.number().optional().default(20).describe("Maximum results"),
168
+ });
169
+
170
+ const DatabaseQuerySchema = z.object({
171
+ sql: z.string().describe("SQL query to execute"),
172
+ params: z.array(z.unknown()).optional().describe("Query parameters"),
173
+ });
174
+
175
+ // Register tools
176
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
177
+ tools: [
178
+ {
179
+ name: "file_search",
180
+ description: "Search for files matching a query",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ query: { type: "string", description: "Search query" },
185
+ directory: { type: "string", description: "Directory to search" },
186
+ fileTypes: {
187
+ type: "array",
188
+ items: { type: "string" },
189
+ description: "File extensions",
190
+ },
191
+ maxResults: {
192
+ type: "number",
193
+ description: "Max results",
194
+ default: 20,
195
+ },
196
+ },
197
+ required: ["query"],
198
+ },
199
+ },
200
+ {
201
+ name: "database_query",
202
+ description: "Execute a read-only database query",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ sql: { type: "string", description: "SQL query" },
207
+ params: { type: "array", description: "Query parameters" },
208
+ },
209
+ required: ["sql"],
210
+ },
211
+ },
212
+ ],
213
+ }));
214
+
215
+ // Handle tool calls
216
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
217
+ const { name, arguments: args } = request.params;
218
+
219
+ try {
220
+ switch (name) {
221
+ case "file_search": {
222
+ const params = FileSearchSchema.parse(args);
223
+ const results = await searchFiles(params);
224
+ return {
225
+ content: [
226
+ {
227
+ type: "text",
228
+ text: JSON.stringify(results, null, 2),
229
+ },
230
+ ],
231
+ };
232
+ }
233
+
234
+ case "database_query": {
235
+ const params = DatabaseQuerySchema.parse(args);
236
+ // Validate read-only
237
+ if (!isReadOnlyQuery(params.sql)) {
238
+ throw new Error("Only SELECT queries are allowed");
239
+ }
240
+ const results = await executeQuery(params.sql, params.params);
241
+ return {
242
+ content: [
243
+ {
244
+ type: "text",
245
+ text: JSON.stringify(results, null, 2),
246
+ },
247
+ ],
248
+ };
249
+ }
250
+
251
+ default:
252
+ throw new Error(`Unknown tool: ${name}`);
253
+ }
254
+ } catch (error) {
255
+ return {
256
+ content: [
257
+ {
258
+ type: "text",
259
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
260
+ },
261
+ ],
262
+ isError: true,
263
+ };
264
+ }
265
+ });
266
+
267
+ // Resource handlers
268
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
269
+ resources: [
270
+ {
271
+ uri: "config://app",
272
+ name: "Application Configuration",
273
+ description: "Current application configuration",
274
+ mimeType: "application/json",
275
+ },
276
+ {
277
+ uri: "schema://database",
278
+ name: "Database Schema",
279
+ description: "Database table definitions",
280
+ mimeType: "application/json",
281
+ },
282
+ ],
283
+ }));
284
+
285
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
286
+ const { uri } = request.params;
287
+
288
+ if (uri === "config://app") {
289
+ return {
290
+ contents: [
291
+ {
292
+ uri,
293
+ mimeType: "application/json",
294
+ text: JSON.stringify(getAppConfig(), null, 2),
295
+ },
296
+ ],
297
+ };
298
+ }
299
+
300
+ if (uri === "schema://database") {
301
+ return {
302
+ contents: [
303
+ {
304
+ uri,
305
+ mimeType: "application/json",
306
+ text: JSON.stringify(getDatabaseSchema(), null, 2),
307
+ },
308
+ ],
309
+ };
310
+ }
311
+
312
+ throw new Error(`Unknown resource: ${uri}`);
313
+ });
314
+
315
+ // Start server
316
+ async function main() {
317
+ const transport = new StdioServerTransport();
318
+ await server.connect(transport);
319
+ console.error("MCP server running on stdio");
320
+ }
321
+
322
+ main().catch(console.error);
323
+ ```
324
+
325
+ ### 3. Advanced Tool Patterns
326
+
327
+ ```python
328
+ from fastmcp import FastMCP, Context
329
+ from typing import AsyncGenerator
330
+ import asyncio
331
+
332
+ mcp = FastMCP("advanced-tools")
333
+
334
+ # Context-aware tool
335
+ @mcp.tool()
336
+ async def analyze_file(ctx: Context, file_path: str) -> str:
337
+ """Analyze a file using context for progress reporting.
338
+
339
+ Args:
340
+ ctx: MCP context for progress updates
341
+ file_path: Path to the file to analyze
342
+ """
343
+ # Report progress
344
+ await ctx.report_progress(0, 100, "Starting analysis...")
345
+
346
+ content = await read_file(file_path)
347
+ await ctx.report_progress(25, 100, "File loaded")
348
+
349
+ # Perform analysis
350
+ syntax_check = await check_syntax(content)
351
+ await ctx.report_progress(50, 100, "Syntax checked")
352
+
353
+ security_scan = await scan_security(content)
354
+ await ctx.report_progress(75, 100, "Security scanned")
355
+
356
+ quality_metrics = await analyze_quality(content)
357
+ await ctx.report_progress(100, 100, "Analysis complete")
358
+
359
+ return json.dumps({
360
+ "syntax": syntax_check,
361
+ "security": security_scan,
362
+ "quality": quality_metrics,
363
+ }, indent=2)
364
+
365
+ # Streaming tool for long operations
366
+ @mcp.tool()
367
+ async def stream_logs(
368
+ log_file: str,
369
+ filter_pattern: str | None = None,
370
+ follow: bool = False
371
+ ) -> AsyncGenerator[str, None]:
372
+ """Stream log file content with optional filtering.
373
+
374
+ Args:
375
+ log_file: Path to log file
376
+ filter_pattern: Regex pattern to filter logs
377
+ follow: Whether to follow new entries (like tail -f)
378
+
379
+ Yields:
380
+ Filtered log lines
381
+ """
382
+ pattern = re.compile(filter_pattern) if filter_pattern else None
383
+
384
+ async with aiofiles.open(log_file, 'r') as f:
385
+ # Read existing content
386
+ async for line in f:
387
+ if pattern is None or pattern.search(line):
388
+ yield line
389
+
390
+ # Follow new content if requested
391
+ if follow:
392
+ while True:
393
+ line = await f.readline()
394
+ if line:
395
+ if pattern is None or pattern.search(line):
396
+ yield line
397
+ else:
398
+ await asyncio.sleep(0.1)
399
+
400
+ # Tool with complex return type
401
+ @mcp.tool()
402
+ def query_with_pagination(
403
+ query: str,
404
+ page: int = 1,
405
+ page_size: int = 20
406
+ ) -> dict:
407
+ """Query data with pagination support.
408
+
409
+ Args:
410
+ query: Search query
411
+ page: Page number (1-indexed)
412
+ page_size: Items per page
413
+
414
+ Returns:
415
+ Paginated results with metadata
416
+ """
417
+ offset = (page - 1) * page_size
418
+ results = db.search(query, limit=page_size, offset=offset)
419
+ total = db.count(query)
420
+
421
+ return {
422
+ "data": results,
423
+ "pagination": {
424
+ "page": page,
425
+ "page_size": page_size,
426
+ "total_items": total,
427
+ "total_pages": (total + page_size - 1) // page_size,
428
+ "has_next": page * page_size < total,
429
+ "has_prev": page > 1,
430
+ }
431
+ }
432
+
433
+ # Error handling wrapper
434
+ @mcp.tool()
435
+ def safe_operation(operation: str, params: dict) -> dict:
436
+ """Execute operation with comprehensive error handling.
437
+
438
+ Args:
439
+ operation: Operation name to execute
440
+ params: Operation parameters
441
+
442
+ Returns:
443
+ Operation result or error details
444
+ """
445
+ try:
446
+ result = execute_operation(operation, params)
447
+ return {
448
+ "success": True,
449
+ "data": result,
450
+ "timestamp": datetime.utcnow().isoformat(),
451
+ }
452
+ except ValidationError as e:
453
+ return {
454
+ "success": False,
455
+ "error": "validation_error",
456
+ "message": str(e),
457
+ "fields": e.errors(),
458
+ }
459
+ except PermissionError as e:
460
+ return {
461
+ "success": False,
462
+ "error": "permission_denied",
463
+ "message": str(e),
464
+ }
465
+ except Exception as e:
466
+ logger.exception("Operation failed")
467
+ return {
468
+ "success": False,
469
+ "error": "internal_error",
470
+ "message": "An unexpected error occurred",
471
+ "reference_id": generate_error_id(),
472
+ }
473
+ ```
474
+
475
+ ### 4. Resource Management
476
+
477
+ ```python
478
+ from fastmcp import FastMCP
479
+ from typing import Optional
480
+ import yaml
481
+
482
+ mcp = FastMCP("resource-server")
483
+
484
+ # Static resource
485
+ @mcp.resource("config://database")
486
+ def database_config() -> str:
487
+ """Database configuration resource."""
488
+ return yaml.dump({
489
+ "host": os.getenv("DB_HOST", "localhost"),
490
+ "port": int(os.getenv("DB_PORT", 5432)),
491
+ "database": os.getenv("DB_NAME", "app"),
492
+ "pool_size": 10,
493
+ })
494
+
495
+ # Dynamic resource with template
496
+ @mcp.resource("table://{schema}/{table}/schema")
497
+ def table_schema(schema: str, table: str) -> str:
498
+ """Get schema definition for a specific table.
499
+
500
+ Args:
501
+ schema: Database schema name
502
+ table: Table name
503
+ """
504
+ columns = db.get_table_schema(schema, table)
505
+ return json.dumps({
506
+ "schema": schema,
507
+ "table": table,
508
+ "columns": [
509
+ {
510
+ "name": col.name,
511
+ "type": col.type,
512
+ "nullable": col.nullable,
513
+ "default": col.default,
514
+ "primary_key": col.primary_key,
515
+ }
516
+ for col in columns
517
+ ],
518
+ "indexes": db.get_indexes(schema, table),
519
+ "foreign_keys": db.get_foreign_keys(schema, table),
520
+ }, indent=2)
521
+
522
+ # Resource with MIME type
523
+ @mcp.resource("file://{path}", mime_type="text/plain")
524
+ def file_content(path: str) -> str:
525
+ """Read file content as resource.
526
+
527
+ Args:
528
+ path: File path relative to workspace
529
+ """
530
+ full_path = os.path.join(WORKSPACE_ROOT, path)
531
+
532
+ # Security: ensure path is within workspace
533
+ if not os.path.realpath(full_path).startswith(WORKSPACE_ROOT):
534
+ raise ValueError("Path outside workspace")
535
+
536
+ with open(full_path, 'r') as f:
537
+ return f.read()
538
+
539
+ # Binary resource
540
+ @mcp.resource("image://{image_id}", mime_type="image/png")
541
+ def get_image(image_id: str) -> bytes:
542
+ """Get image data as binary resource.
543
+
544
+ Args:
545
+ image_id: Image identifier
546
+ """
547
+ return storage.get_image(image_id)
548
+
549
+ # Aggregated resource
550
+ @mcp.resource("project://overview")
551
+ def project_overview() -> str:
552
+ """Get complete project overview."""
553
+ return json.dumps({
554
+ "name": project.name,
555
+ "version": project.version,
556
+ "dependencies": project.get_dependencies(),
557
+ "scripts": project.get_scripts(),
558
+ "structure": project.get_directory_tree(),
559
+ "recent_changes": project.get_recent_commits(10),
560
+ }, indent=2)
561
+ ```
562
+
563
+ ### 5. Server Lifecycle and Configuration
564
+
565
+ ```python
566
+ from fastmcp import FastMCP
567
+ from contextlib import asynccontextmanager
568
+
569
+ # Server with lifecycle hooks
570
+ @asynccontextmanager
571
+ async def lifespan(server: FastMCP):
572
+ """Manage server lifecycle."""
573
+ # Startup
574
+ print("Starting MCP server...")
575
+ await database.connect()
576
+ await cache.connect()
577
+
578
+ yield # Server is running
579
+
580
+ # Shutdown
581
+ print("Shutting down MCP server...")
582
+ await database.disconnect()
583
+ await cache.disconnect()
584
+
585
+ mcp = FastMCP("lifecycle-server", lifespan=lifespan)
586
+
587
+ # Configuration management
588
+ class ServerConfig:
589
+ def __init__(self):
590
+ self.debug = os.getenv("DEBUG", "false").lower() == "true"
591
+ self.max_connections = int(os.getenv("MAX_CONNECTIONS", 100))
592
+ self.timeout = int(os.getenv("TIMEOUT", 30))
593
+ self.allowed_paths = os.getenv("ALLOWED_PATHS", ".").split(",")
594
+
595
+ config = ServerConfig()
596
+
597
+ # Middleware for logging
598
+ @mcp.middleware
599
+ async def logging_middleware(request, call_next):
600
+ """Log all tool calls."""
601
+ start_time = time.time()
602
+ logger.info(f"Tool call: {request.method} - {request.params}")
603
+
604
+ response = await call_next(request)
605
+
606
+ duration = time.time() - start_time
607
+ logger.info(f"Completed in {duration:.2f}s")
608
+
609
+ return response
610
+
611
+ # Rate limiting middleware
612
+ @mcp.middleware
613
+ async def rate_limit_middleware(request, call_next):
614
+ """Apply rate limiting."""
615
+ client_id = request.context.get("client_id", "default")
616
+
617
+ if not rate_limiter.allow(client_id):
618
+ raise RateLimitError("Too many requests")
619
+
620
+ return await call_next(request)
621
+
622
+ # Health check resource
623
+ @mcp.resource("health://status")
624
+ def health_status() -> str:
625
+ """Server health status."""
626
+ return json.dumps({
627
+ "status": "healthy",
628
+ "uptime": get_uptime(),
629
+ "connections": {
630
+ "database": database.is_connected(),
631
+ "cache": cache.is_connected(),
632
+ },
633
+ "metrics": {
634
+ "requests_total": metrics.requests_total,
635
+ "errors_total": metrics.errors_total,
636
+ "avg_response_time": metrics.avg_response_time,
637
+ },
638
+ })
639
+ ```
640
+
641
+ ### 6. Testing MCP Servers
642
+
643
+ ```python
644
+ import pytest
645
+ from fastmcp.testing import MCPTestClient
646
+
647
+ @pytest.fixture
648
+ def client():
649
+ """Create test client for MCP server."""
650
+ return MCPTestClient(mcp)
651
+
652
+ class TestMCPServer:
653
+ async def test_tool_execution(self, client):
654
+ """Test basic tool execution."""
655
+ result = await client.call_tool("get_weather", {"city": "Seattle"})
656
+
657
+ assert result.success
658
+ assert "Temperature" in result.content
659
+ assert "Seattle" in result.content
660
+
661
+ async def test_tool_validation(self, client):
662
+ """Test parameter validation."""
663
+ result = await client.call_tool("get_weather", {})
664
+
665
+ assert not result.success
666
+ assert "city" in result.error.lower()
667
+
668
+ async def test_resource_access(self, client):
669
+ """Test resource retrieval."""
670
+ result = await client.read_resource("config://database")
671
+
672
+ assert result.success
673
+ data = json.loads(result.content)
674
+ assert "host" in data
675
+ assert "port" in data
676
+
677
+ async def test_dynamic_resource(self, client):
678
+ """Test dynamic resource with parameters."""
679
+ result = await client.read_resource("table://public/users/schema")
680
+
681
+ assert result.success
682
+ schema = json.loads(result.content)
683
+ assert schema["table"] == "users"
684
+ assert "columns" in schema
685
+
686
+ async def test_error_handling(self, client):
687
+ """Test error responses."""
688
+ result = await client.call_tool("safe_operation", {
689
+ "operation": "invalid",
690
+ "params": {},
691
+ })
692
+
693
+ assert result.content["success"] is False
694
+ assert "error" in result.content
695
+
696
+ # Integration test with mock
697
+ class TestMCPIntegration:
698
+ async def test_full_workflow(self, client, mock_db):
699
+ """Test complete workflow."""
700
+ # Setup test data
701
+ mock_db.insert_users([
702
+ {"id": 1, "name": "Alice"},
703
+ {"id": 2, "name": "Bob"},
704
+ ])
705
+
706
+ # Search users
707
+ result = await client.call_tool("search_database", {
708
+ "query": "Alice",
709
+ "table": "users",
710
+ })
711
+
712
+ assert result.success
713
+ users = json.loads(result.content)
714
+ assert len(users) == 1
715
+ assert users[0]["name"] == "Alice"
716
+ ```
717
+
718
+ ## Use Cases
719
+
720
+ ### 1. Database Integration Server
721
+
722
+ ```python
723
+ # Complete database integration MCP server
724
+ from fastmcp import FastMCP
725
+ import asyncpg
726
+
727
+ mcp = FastMCP("database-server")
728
+
729
+ # Connection pool
730
+ pool: asyncpg.Pool = None
731
+
732
+ @mcp.tool()
733
+ async def query(sql: str, params: list = None) -> dict:
734
+ """Execute read-only SQL query."""
735
+ if not sql.strip().upper().startswith("SELECT"):
736
+ raise ValueError("Only SELECT queries allowed")
737
+
738
+ async with pool.acquire() as conn:
739
+ rows = await conn.fetch(sql, *(params or []))
740
+ return {
741
+ "columns": list(rows[0].keys()) if rows else [],
742
+ "rows": [dict(row) for row in rows],
743
+ "count": len(rows),
744
+ }
745
+
746
+ @mcp.tool()
747
+ async def describe_table(table_name: str) -> dict:
748
+ """Get table structure."""
749
+ async with pool.acquire() as conn:
750
+ columns = await conn.fetch("""
751
+ SELECT column_name, data_type, is_nullable
752
+ FROM information_schema.columns
753
+ WHERE table_name = $1
754
+ """, table_name)
755
+
756
+ return {
757
+ "table": table_name,
758
+ "columns": [dict(col) for col in columns],
759
+ }
760
+
761
+ @mcp.resource("schema://tables")
762
+ async def list_tables() -> str:
763
+ """List all database tables."""
764
+ async with pool.acquire() as conn:
765
+ tables = await conn.fetch("""
766
+ SELECT table_name
767
+ FROM information_schema.tables
768
+ WHERE table_schema = 'public'
769
+ """)
770
+ return json.dumps([t["table_name"] for t in tables])
771
+ ```
772
+
773
+ ### 2. File System Server
774
+
775
+ ```python
776
+ # Secure file system access server
777
+ from fastmcp import FastMCP
778
+ from pathlib import Path
779
+
780
+ mcp = FastMCP("filesystem-server")
781
+ WORKSPACE = Path(os.getenv("WORKSPACE", ".")).resolve()
782
+
783
+ def validate_path(path: str) -> Path:
784
+ """Ensure path is within workspace."""
785
+ full_path = (WORKSPACE / path).resolve()
786
+ if not str(full_path).startswith(str(WORKSPACE)):
787
+ raise ValueError("Path outside workspace")
788
+ return full_path
789
+
790
+ @mcp.tool()
791
+ def list_directory(path: str = ".") -> list[dict]:
792
+ """List directory contents."""
793
+ dir_path = validate_path(path)
794
+
795
+ return [
796
+ {
797
+ "name": item.name,
798
+ "type": "directory" if item.is_dir() else "file",
799
+ "size": item.stat().st_size if item.is_file() else None,
800
+ "modified": item.stat().st_mtime,
801
+ }
802
+ for item in dir_path.iterdir()
803
+ ]
804
+
805
+ @mcp.tool()
806
+ def read_file(path: str) -> str:
807
+ """Read file contents."""
808
+ file_path = validate_path(path)
809
+ return file_path.read_text()
810
+
811
+ @mcp.tool()
812
+ def search_files(pattern: str, path: str = ".") -> list[str]:
813
+ """Search for files matching pattern."""
814
+ search_path = validate_path(path)
815
+ return [
816
+ str(p.relative_to(WORKSPACE))
817
+ for p in search_path.rglob(pattern)
818
+ ]
819
+ ```
820
+
821
+ ## Best Practices
822
+
823
+ ### Do's
824
+
825
+ - **Document thoroughly** - Clear docstrings become tool descriptions
826
+ - **Validate inputs** - Use type hints and validation
827
+ - **Handle errors gracefully** - Return structured error responses
828
+ - **Use async** - For I/O-bound operations
829
+ - **Test comprehensively** - Use MCPTestClient for testing
830
+ - **Follow MCP spec** - Adhere to protocol specification
831
+
832
+ ### Don'ts
833
+
834
+ - Don't expose sensitive operations without authorization
835
+ - Don't allow arbitrary code execution
836
+ - Don't return unbounded data (use pagination)
837
+ - Don't ignore rate limiting for resource-intensive tools
838
+ - Don't skip input sanitization
839
+ - Don't expose internal error details
840
+
841
+ ### Security Checklist
842
+
843
+ ```python
844
+ # Security validation helper
845
+ class SecurityValidator:
846
+ @staticmethod
847
+ def validate_sql(query: str) -> bool:
848
+ """Ensure SQL is read-only."""
849
+ dangerous = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE"]
850
+ upper = query.upper()
851
+ return not any(d in upper for d in dangerous)
852
+
853
+ @staticmethod
854
+ def validate_path(path: str, allowed_root: Path) -> Path:
855
+ """Ensure path is within allowed directory."""
856
+ resolved = (allowed_root / path).resolve()
857
+ if not str(resolved).startswith(str(allowed_root.resolve())):
858
+ raise SecurityError("Path traversal attempt detected")
859
+ return resolved
860
+
861
+ @staticmethod
862
+ def sanitize_output(data: dict) -> dict:
863
+ """Remove sensitive fields from output."""
864
+ sensitive_keys = {"password", "secret", "token", "api_key"}
865
+ return {
866
+ k: v for k, v in data.items()
867
+ if k.lower() not in sensitive_keys
868
+ }
869
+ ```
870
+
871
+ ## Related Skills
872
+
873
+ - **python** - Primary language for FastMCP
874
+ - **typescript** - MCP SDK for TypeScript
875
+ - **api-architecture** - API design patterns
876
+ - **backend-development** - Server development patterns
877
+
878
+ ## Reference Resources
879
+
880
+ - [MCP Specification](https://spec.modelcontextprotocol.io/)
881
+ - [FastMCP Documentation](https://github.com/jlowin/fastmcp)
882
+ - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
883
+ - [Claude MCP Guide](https://docs.anthropic.com/en/docs/build-with-claude/mcp)