omgkit 2.0.7 → 2.1.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.
- package/package.json +2 -2
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -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)
|