-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmcp-server-sdk.py
More file actions
executable file
·337 lines (290 loc) · 11.5 KB
/
mcp-server-sdk.py
File metadata and controls
executable file
·337 lines (290 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/env python3
"""
DevOps Practices MCP Server (using official MCP SDK)
Provides shared DevOps practices and templates for all example-project infrastructure projects.
"""
import asyncio
import logging
import os
import re
from datetime import datetime
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# Configure logging to file (to avoid interfering with stdio protocol)
log_dir = os.path.expanduser('~/.cache/claude')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'mcp-devops-practices.log')
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.FileHandler(log_file)]
)
logger = logging.getLogger('devops-practices')
# Base directory (where this script is located)
BASE_DIR = Path(__file__).parent.absolute()
PRACTICES_DIR = BASE_DIR / 'practices'
TEMPLATES_DIR = BASE_DIR / 'templates'
# Create the MCP server
app = Server("devops-practices")
def load_practices() -> dict[str, str]:
"""Load all practice files from practices directory."""
practices = {}
if not PRACTICES_DIR.exists():
logger.warning(f"Practices directory not found: {PRACTICES_DIR}")
return practices
for file_path in PRACTICES_DIR.glob('*.md'):
practice_name = file_path.stem
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
practices[practice_name] = content
logger.info(f"Loaded practice: {practice_name} ({len(content)} chars)")
except Exception as e:
logger.error(f"Error loading practice {practice_name}: {e}")
return practices
def load_templates() -> dict[str, str]:
"""Load all template files from templates directory."""
templates = {}
if not TEMPLATES_DIR.exists():
logger.warning(f"Templates directory not found: {TEMPLATES_DIR}")
return templates
for file_path in TEMPLATES_DIR.glob('*.md'):
template_name = file_path.stem
try:
with open(file_path, 'r', encoding='utf-8') as f:
templates[template_name] = f.read()
logger.info(f"Loaded template: {template_name}")
except Exception as e:
logger.error(f"Error loading template {template_name}: {e}")
return templates
# Load all practices and templates at startup
PRACTICES = load_practices()
TEMPLATES = load_templates()
logger.info(f"Loaded {len(PRACTICES)} practices and {len(TEMPLATES)} templates")
@app.list_tools()
async def list_tools() -> list[Tool]:
"""List all available tools."""
return [
Tool(
name="get_practice",
description="Get a DevOps practice document by name",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": 'Name of the practice (e.g., "air-gapped-workflow", "documentation-standards")'
}
},
"required": ["name"]
}
),
Tool(
name="get_practice_summary",
description="Get a brief summary of a practice (first ~500 chars). Lighter than get_practice, good for quick reference.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the practice"
},
"max_chars": {
"type": "integer",
"description": "Maximum characters to return (default: 500)",
"default": 500
}
},
"required": ["name"]
}
),
Tool(
name="list_practices",
description="List all available DevOps practices with metadata",
inputSchema={
"type": "object",
"properties": {}
}
),
Tool(
name="search_practices",
description="Search practices by keyword in name or content",
inputSchema={
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "Keyword to search for"
}
},
"required": ["keyword"]
}
),
Tool(
name="get_template",
description="Get a file template by name",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": 'Name of the template (e.g., "TRACKER-template", "CURRENT-STATE-template")'
}
},
"required": ["name"]
}
),
Tool(
name="list_templates",
description="List all available file templates",
inputSchema={
"type": "object",
"properties": {}
}
),
Tool(
name="render_template",
description="Render a template with variable substitution. Supports ${VAR} format. Auto-provides DATE, TIMESTAMP, USER, YEAR.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": 'Name of the template (e.g., "TRACKER-template", "RUNBOOK-template")'
},
"variables": {
"type": "object",
"description": 'Dictionary of variables to substitute (e.g., {"PROJECT_NAME": "my-project", "SESSION_NUMBER": "1"})',
"additionalProperties": {
"type": "string"
}
}
},
"required": ["name"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool calls."""
logger.info(f"Tool called: {name} with args: {arguments}")
if name == "list_practices":
practices_list = []
for practice_name, content in PRACTICES.items():
# Extract title from first heading
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
title = title_match.group(1) if title_match else practice_name
practices_list.append({
'name': practice_name,
'title': title,
'size': f'{len(content)} chars'
})
# Sort by name
practices_list.sort(key=lambda x: x['name'])
text = "Available DevOps Practices:\n\n"
for practice in practices_list:
text += f"• **{practice['name']}**\n"
text += f" Title: {practice['title']}\n"
text += f" Size: {practice['size']}\n\n"
return [TextContent(type="text", text=text)]
elif name == "get_practice":
practice_name = arguments.get("name", "")
content = PRACTICES.get(practice_name)
if content:
return [TextContent(type="text", text=content)]
else:
available = ', '.join(PRACTICES.keys())
raise ValueError(f'Practice not found: {practice_name}. Available: {available}')
elif name == "get_practice_summary":
practice_name = arguments.get("name", "")
max_chars = arguments.get("max_chars", 500)
content = PRACTICES.get(practice_name)
if content:
summary = content[:max_chars]
if len(content) > max_chars:
summary += "..."
return [TextContent(type="text", text=summary)]
else:
available = ', '.join(PRACTICES.keys())
raise ValueError(f'Practice not found: {practice_name}. Available: {available}')
elif name == "search_practices":
keyword = arguments.get("keyword", "").lower()
if not keyword:
raise ValueError("keyword parameter is required")
results = []
for practice_name, content in PRACTICES.items():
# Search in name
if keyword in practice_name.lower():
results.append(practice_name)
continue
# Search in content
if keyword in content.lower():
results.append(practice_name)
if results:
text = f"Found {len(results)} practice(s) matching '{keyword}':\n\n"
for practice_name in results:
# Extract title
content = PRACTICES[practice_name]
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
title = title_match.group(1) if title_match else practice_name
text += f"• {practice_name}: {title}\n"
return [TextContent(type="text", text=text)]
else:
return [TextContent(type="text", text=f"No practices found matching '{keyword}'")]
elif name == "list_templates":
templates_list = list(TEMPLATES.keys())
text = "Available templates:\n" + '\n'.join(f'- {t}' for t in templates_list)
return [TextContent(type="text", text=text)]
elif name == "get_template":
template_name = arguments.get("name", "")
content = TEMPLATES.get(template_name)
if content:
return [TextContent(type="text", text=content)]
else:
available = ', '.join(TEMPLATES.keys())
raise ValueError(f'Template not found: {template_name}. Available: {available}')
elif name == "render_template":
template_name = arguments.get("name", "")
variables = arguments.get("variables", {})
template = TEMPLATES.get(template_name)
if not template:
available = ', '.join(TEMPLATES.keys())
raise ValueError(f'Template not found: {template_name}. Available: {available}')
# Default variables
try:
now_utc = datetime.now(datetime.UTC)
except AttributeError:
now_utc = datetime.utcnow()
defaults = {
'DATE': now_utc.strftime('%Y-%m-%d'),
'TIMESTAMP': now_utc.strftime('%Y%m%dT%H%MZ'),
'USER': os.getenv('USER', 'user'),
'YEAR': str(now_utc.year),
}
# Merge user variables with defaults
all_variables = {**defaults, **variables}
# Perform substitution
rendered = template
for key, value in all_variables.items():
rendered = rendered.replace(f'${{{key}}}', value)
rendered = rendered.replace(f'${key}', value)
return [TextContent(type="text", text=rendered)]
else:
raise ValueError(f'Unknown tool: {name}')
async def main():
"""Run the MCP server."""
async with stdio_server() as (read_stream, write_stream):
logger.info("Starting DevOps Practices MCP Server")
logger.info(f"Base directory: {BASE_DIR}")
logger.info(f"Practices loaded: {', '.join(sorted(PRACTICES.keys()))}")
logger.info(f"Templates loaded: {', '.join(sorted(TEMPLATES.keys()))}")
logger.info(f"Log file: {log_file}")
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == '__main__':
asyncio.run(main())