foundry-mcp 0.3.3__py3-none-any.whl

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.
Files changed (135) hide show
  1. foundry_mcp/__init__.py +7 -0
  2. foundry_mcp/cli/__init__.py +80 -0
  3. foundry_mcp/cli/__main__.py +9 -0
  4. foundry_mcp/cli/agent.py +96 -0
  5. foundry_mcp/cli/commands/__init__.py +37 -0
  6. foundry_mcp/cli/commands/cache.py +137 -0
  7. foundry_mcp/cli/commands/dashboard.py +148 -0
  8. foundry_mcp/cli/commands/dev.py +446 -0
  9. foundry_mcp/cli/commands/journal.py +377 -0
  10. foundry_mcp/cli/commands/lifecycle.py +274 -0
  11. foundry_mcp/cli/commands/modify.py +824 -0
  12. foundry_mcp/cli/commands/plan.py +633 -0
  13. foundry_mcp/cli/commands/pr.py +393 -0
  14. foundry_mcp/cli/commands/review.py +652 -0
  15. foundry_mcp/cli/commands/session.py +479 -0
  16. foundry_mcp/cli/commands/specs.py +856 -0
  17. foundry_mcp/cli/commands/tasks.py +807 -0
  18. foundry_mcp/cli/commands/testing.py +676 -0
  19. foundry_mcp/cli/commands/validate.py +982 -0
  20. foundry_mcp/cli/config.py +98 -0
  21. foundry_mcp/cli/context.py +259 -0
  22. foundry_mcp/cli/flags.py +266 -0
  23. foundry_mcp/cli/logging.py +212 -0
  24. foundry_mcp/cli/main.py +44 -0
  25. foundry_mcp/cli/output.py +122 -0
  26. foundry_mcp/cli/registry.py +110 -0
  27. foundry_mcp/cli/resilience.py +178 -0
  28. foundry_mcp/cli/transcript.py +217 -0
  29. foundry_mcp/config.py +850 -0
  30. foundry_mcp/core/__init__.py +144 -0
  31. foundry_mcp/core/ai_consultation.py +1636 -0
  32. foundry_mcp/core/cache.py +195 -0
  33. foundry_mcp/core/capabilities.py +446 -0
  34. foundry_mcp/core/concurrency.py +898 -0
  35. foundry_mcp/core/context.py +540 -0
  36. foundry_mcp/core/discovery.py +1603 -0
  37. foundry_mcp/core/error_collection.py +728 -0
  38. foundry_mcp/core/error_store.py +592 -0
  39. foundry_mcp/core/feature_flags.py +592 -0
  40. foundry_mcp/core/health.py +749 -0
  41. foundry_mcp/core/journal.py +694 -0
  42. foundry_mcp/core/lifecycle.py +412 -0
  43. foundry_mcp/core/llm_config.py +1350 -0
  44. foundry_mcp/core/llm_patterns.py +510 -0
  45. foundry_mcp/core/llm_provider.py +1569 -0
  46. foundry_mcp/core/logging_config.py +374 -0
  47. foundry_mcp/core/metrics_persistence.py +584 -0
  48. foundry_mcp/core/metrics_registry.py +327 -0
  49. foundry_mcp/core/metrics_store.py +641 -0
  50. foundry_mcp/core/modifications.py +224 -0
  51. foundry_mcp/core/naming.py +123 -0
  52. foundry_mcp/core/observability.py +1216 -0
  53. foundry_mcp/core/otel.py +452 -0
  54. foundry_mcp/core/otel_stubs.py +264 -0
  55. foundry_mcp/core/pagination.py +255 -0
  56. foundry_mcp/core/progress.py +317 -0
  57. foundry_mcp/core/prometheus.py +577 -0
  58. foundry_mcp/core/prompts/__init__.py +464 -0
  59. foundry_mcp/core/prompts/fidelity_review.py +546 -0
  60. foundry_mcp/core/prompts/markdown_plan_review.py +511 -0
  61. foundry_mcp/core/prompts/plan_review.py +623 -0
  62. foundry_mcp/core/providers/__init__.py +225 -0
  63. foundry_mcp/core/providers/base.py +476 -0
  64. foundry_mcp/core/providers/claude.py +460 -0
  65. foundry_mcp/core/providers/codex.py +619 -0
  66. foundry_mcp/core/providers/cursor_agent.py +642 -0
  67. foundry_mcp/core/providers/detectors.py +488 -0
  68. foundry_mcp/core/providers/gemini.py +405 -0
  69. foundry_mcp/core/providers/opencode.py +616 -0
  70. foundry_mcp/core/providers/opencode_wrapper.js +302 -0
  71. foundry_mcp/core/providers/package-lock.json +24 -0
  72. foundry_mcp/core/providers/package.json +25 -0
  73. foundry_mcp/core/providers/registry.py +607 -0
  74. foundry_mcp/core/providers/test_provider.py +171 -0
  75. foundry_mcp/core/providers/validation.py +729 -0
  76. foundry_mcp/core/rate_limit.py +427 -0
  77. foundry_mcp/core/resilience.py +600 -0
  78. foundry_mcp/core/responses.py +934 -0
  79. foundry_mcp/core/review.py +366 -0
  80. foundry_mcp/core/security.py +438 -0
  81. foundry_mcp/core/spec.py +1650 -0
  82. foundry_mcp/core/task.py +1289 -0
  83. foundry_mcp/core/testing.py +450 -0
  84. foundry_mcp/core/validation.py +2081 -0
  85. foundry_mcp/dashboard/__init__.py +32 -0
  86. foundry_mcp/dashboard/app.py +119 -0
  87. foundry_mcp/dashboard/components/__init__.py +17 -0
  88. foundry_mcp/dashboard/components/cards.py +88 -0
  89. foundry_mcp/dashboard/components/charts.py +234 -0
  90. foundry_mcp/dashboard/components/filters.py +136 -0
  91. foundry_mcp/dashboard/components/tables.py +195 -0
  92. foundry_mcp/dashboard/data/__init__.py +11 -0
  93. foundry_mcp/dashboard/data/stores.py +433 -0
  94. foundry_mcp/dashboard/launcher.py +289 -0
  95. foundry_mcp/dashboard/views/__init__.py +12 -0
  96. foundry_mcp/dashboard/views/errors.py +217 -0
  97. foundry_mcp/dashboard/views/metrics.py +174 -0
  98. foundry_mcp/dashboard/views/overview.py +160 -0
  99. foundry_mcp/dashboard/views/providers.py +83 -0
  100. foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
  101. foundry_mcp/dashboard/views/tool_usage.py +139 -0
  102. foundry_mcp/prompts/__init__.py +9 -0
  103. foundry_mcp/prompts/workflows.py +525 -0
  104. foundry_mcp/resources/__init__.py +9 -0
  105. foundry_mcp/resources/specs.py +591 -0
  106. foundry_mcp/schemas/__init__.py +38 -0
  107. foundry_mcp/schemas/sdd-spec-schema.json +386 -0
  108. foundry_mcp/server.py +164 -0
  109. foundry_mcp/tools/__init__.py +10 -0
  110. foundry_mcp/tools/unified/__init__.py +71 -0
  111. foundry_mcp/tools/unified/authoring.py +1487 -0
  112. foundry_mcp/tools/unified/context_helpers.py +98 -0
  113. foundry_mcp/tools/unified/documentation_helpers.py +198 -0
  114. foundry_mcp/tools/unified/environment.py +939 -0
  115. foundry_mcp/tools/unified/error.py +462 -0
  116. foundry_mcp/tools/unified/health.py +225 -0
  117. foundry_mcp/tools/unified/journal.py +841 -0
  118. foundry_mcp/tools/unified/lifecycle.py +632 -0
  119. foundry_mcp/tools/unified/metrics.py +777 -0
  120. foundry_mcp/tools/unified/plan.py +745 -0
  121. foundry_mcp/tools/unified/pr.py +294 -0
  122. foundry_mcp/tools/unified/provider.py +629 -0
  123. foundry_mcp/tools/unified/review.py +685 -0
  124. foundry_mcp/tools/unified/review_helpers.py +299 -0
  125. foundry_mcp/tools/unified/router.py +102 -0
  126. foundry_mcp/tools/unified/server.py +580 -0
  127. foundry_mcp/tools/unified/spec.py +808 -0
  128. foundry_mcp/tools/unified/task.py +2202 -0
  129. foundry_mcp/tools/unified/test.py +370 -0
  130. foundry_mcp/tools/unified/verification.py +520 -0
  131. foundry_mcp-0.3.3.dist-info/METADATA +337 -0
  132. foundry_mcp-0.3.3.dist-info/RECORD +135 -0
  133. foundry_mcp-0.3.3.dist-info/WHEEL +4 -0
  134. foundry_mcp-0.3.3.dist-info/entry_points.txt +3 -0
  135. foundry_mcp-0.3.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,580 @@
1
+ """Unified server discovery tool with action routing.
2
+
3
+ Consolidates discovery/context helpers into a single `server(action=...)` tool.
4
+ Only the unified tool surface is exposed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import time
12
+ from dataclasses import asdict
13
+ from functools import lru_cache
14
+ from typing import Any, Dict, Optional
15
+
16
+ from mcp.server.fastmcp import FastMCP
17
+
18
+ from foundry_mcp.config import ServerConfig
19
+ from foundry_mcp.core.context import generate_correlation_id, get_correlation_id
20
+ from foundry_mcp.core.discovery import get_capabilities, get_tool_registry
21
+ from foundry_mcp.core.feature_flags import get_flag_service
22
+ from foundry_mcp.core.naming import canonical_tool
23
+ from foundry_mcp.core.observability import (
24
+ get_metrics,
25
+ get_observability_manager,
26
+ mcp_tool,
27
+ )
28
+ from foundry_mcp.core.pagination import (
29
+ DEFAULT_PAGE_SIZE,
30
+ MAX_PAGE_SIZE,
31
+ CursorError,
32
+ decode_cursor,
33
+ encode_cursor,
34
+ paginated_response,
35
+ )
36
+ from foundry_mcp.core.responses import (
37
+ ErrorCode,
38
+ ErrorType,
39
+ error_response,
40
+ success_response,
41
+ )
42
+ from foundry_mcp.tools.unified.context_helpers import (
43
+ build_llm_status_response,
44
+ build_server_context_response,
45
+ )
46
+ from foundry_mcp.tools.unified.router import (
47
+ ActionDefinition,
48
+ ActionRouter,
49
+ ActionRouterError,
50
+ )
51
+
52
+ logger = logging.getLogger(__name__)
53
+ _metrics = get_metrics()
54
+
55
+
56
+ def _request_id() -> str:
57
+ return get_correlation_id() or generate_correlation_id(prefix="server")
58
+
59
+
60
+ def _metric(action: str) -> str:
61
+ return f"unified_tools.server.{action.replace('-', '_')}"
62
+
63
+
64
+ MANIFEST_TOKEN_BUDGET = 16_000
65
+ MANIFEST_TOKEN_BUDGET_MAX = 18_000
66
+
67
+
68
+ @lru_cache(maxsize=1)
69
+ def _get_tokenizer() -> Any | None:
70
+ try:
71
+ import tiktoken
72
+
73
+ return tiktoken.get_encoding("cl100k_base")
74
+ except Exception:
75
+ return None
76
+
77
+
78
+ def _estimate_tokens(text: str) -> int:
79
+ """Estimate token usage for manifest budget dashboards.
80
+
81
+ Uses `tiktoken` when available, otherwise falls back to a conservative
82
+ ~4-chars-per-token heuristic.
83
+ """
84
+
85
+ tokenizer = _get_tokenizer()
86
+ if tokenizer is not None:
87
+ return len(tokenizer.encode(text))
88
+
89
+ return max(1, len(text) // 4)
90
+
91
+
92
+ def _validation_error(
93
+ *, message: str, request_id: str, remediation: Optional[str] = None
94
+ ) -> dict:
95
+ return asdict(
96
+ error_response(
97
+ message,
98
+ error_code=ErrorCode.VALIDATION_ERROR,
99
+ error_type=ErrorType.VALIDATION,
100
+ remediation=remediation,
101
+ request_id=request_id,
102
+ )
103
+ )
104
+
105
+
106
+ def _build_unified_manifest_tools() -> list[Dict[str, Any]]:
107
+ """Return compact tool entries for the 16-tool unified manifest."""
108
+
109
+ from foundry_mcp.tools.unified.authoring import _AUTHORING_ROUTER
110
+ from foundry_mcp.tools.unified.environment import _ENVIRONMENT_ROUTER
111
+ from foundry_mcp.tools.unified.error import _ERROR_ROUTER
112
+ from foundry_mcp.tools.unified.health import _HEALTH_ROUTER
113
+ from foundry_mcp.tools.unified.journal import _JOURNAL_ROUTER
114
+ from foundry_mcp.tools.unified.lifecycle import _LIFECYCLE_ROUTER
115
+ from foundry_mcp.tools.unified.metrics import _METRICS_ROUTER
116
+ from foundry_mcp.tools.unified.plan import _PLAN_ROUTER
117
+ from foundry_mcp.tools.unified.pr import _PR_ROUTER
118
+ from foundry_mcp.tools.unified.provider import _PROVIDER_ROUTER
119
+ from foundry_mcp.tools.unified.review import _REVIEW_ROUTER
120
+ from foundry_mcp.tools.unified.spec import _SPEC_ROUTER
121
+ from foundry_mcp.tools.unified.task import _TASK_ROUTER
122
+ from foundry_mcp.tools.unified.test import _TEST_ROUTER
123
+ from foundry_mcp.tools.unified.verification import _VERIFICATION_ROUTER
124
+
125
+ routers = {
126
+ "health": _HEALTH_ROUTER,
127
+ "plan": _PLAN_ROUTER,
128
+ "pr": _PR_ROUTER,
129
+ "error": _ERROR_ROUTER,
130
+ "metrics": _METRICS_ROUTER,
131
+ "journal": _JOURNAL_ROUTER,
132
+ "authoring": _AUTHORING_ROUTER,
133
+ "provider": _PROVIDER_ROUTER,
134
+ "environment": _ENVIRONMENT_ROUTER,
135
+ "lifecycle": _LIFECYCLE_ROUTER,
136
+ "verification": _VERIFICATION_ROUTER,
137
+ "task": _TASK_ROUTER,
138
+ "spec": _SPEC_ROUTER,
139
+ "review": _REVIEW_ROUTER,
140
+ "server": _SERVER_ROUTER,
141
+ "test": _TEST_ROUTER,
142
+ }
143
+
144
+ categories = {
145
+ "health": "health",
146
+ "plan": "planning",
147
+ "pr": "workflow",
148
+ "error": "observability",
149
+ "metrics": "observability",
150
+ "journal": "journal",
151
+ "authoring": "specs",
152
+ "provider": "providers",
153
+ "environment": "environment",
154
+ "lifecycle": "lifecycle",
155
+ "verification": "verification",
156
+ "task": "tasks",
157
+ "spec": "specs",
158
+ "review": "review",
159
+ "server": "server",
160
+ "test": "testing",
161
+ }
162
+
163
+ descriptions = {
164
+ "health": "Health checks and diagnostics.",
165
+ "plan": "Planning helpers (create/list/review plans).",
166
+ "pr": "PR workflows with spec context.",
167
+ "error": "Error collection query and cleanup.",
168
+ "metrics": "Metrics query, stats, and cleanup.",
169
+ "journal": "Journaling add/list helpers.",
170
+ "authoring": "Spec authoring mutations (phases, assumptions, revisions).",
171
+ "provider": "LLM provider discovery and execution.",
172
+ "environment": "Workspace init + environment verification.",
173
+ "lifecycle": "Spec lifecycle transitions.",
174
+ "verification": "Verification definition + execution.",
175
+ "task": "Task preparation, mutation, and listing.",
176
+ "spec": "Spec discovery, validation, and analysis.",
177
+ "review": "LLM-assisted review workflows.",
178
+ "server": "Tool discovery, schemas, context, and capabilities.",
179
+ "test": "Pytest discovery and execution.",
180
+ }
181
+
182
+ tools: list[Dict[str, Any]] = []
183
+ for name, router in routers.items():
184
+ summaries = router.describe()
185
+ actions = [
186
+ {"name": action, "summary": summaries.get(action)}
187
+ for action in router.allowed_actions()
188
+ ]
189
+ tools.append(
190
+ {
191
+ "name": name,
192
+ "description": descriptions.get(name, ""),
193
+ "category": categories.get(name, "general"),
194
+ "version": "1.0.0",
195
+ "deprecated": False,
196
+ "tags": ["unified"],
197
+ "actions": actions,
198
+ }
199
+ )
200
+
201
+ return tools
202
+
203
+
204
+ def _handle_tools(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
205
+ request_id = _request_id()
206
+
207
+ category = payload.get("category")
208
+ if category is not None and not isinstance(category, str):
209
+ return _validation_error(
210
+ message="category must be a string",
211
+ request_id=request_id,
212
+ remediation="Provide a category name like 'specs'",
213
+ )
214
+
215
+ tag = payload.get("tag")
216
+ if tag is not None and not isinstance(tag, str):
217
+ return _validation_error(
218
+ message="tag must be a string",
219
+ request_id=request_id,
220
+ remediation="Provide a tag name like 'read'",
221
+ )
222
+
223
+ include_deprecated_value = payload.get("include_deprecated", False)
224
+ if include_deprecated_value is not None and not isinstance(
225
+ include_deprecated_value, bool
226
+ ):
227
+ return _validation_error(
228
+ message="include_deprecated must be a boolean",
229
+ request_id=request_id,
230
+ remediation="Provide include_deprecated=true|false",
231
+ )
232
+ include_deprecated = (
233
+ include_deprecated_value
234
+ if isinstance(include_deprecated_value, bool)
235
+ else False
236
+ )
237
+
238
+ cursor = payload.get("cursor")
239
+ if cursor is not None and not isinstance(cursor, str):
240
+ return _validation_error(
241
+ message="cursor must be a string",
242
+ request_id=request_id,
243
+ remediation="Use the cursor provided in meta.pagination",
244
+ )
245
+
246
+ limit = payload.get("limit", DEFAULT_PAGE_SIZE)
247
+ try:
248
+ limit_int = int(limit)
249
+ except (TypeError, ValueError):
250
+ return _validation_error(
251
+ message="limit must be an integer",
252
+ request_id=request_id,
253
+ remediation=f"Provide an integer between 1 and {MAX_PAGE_SIZE}",
254
+ )
255
+ limit_int = min(max(1, limit_int), MAX_PAGE_SIZE)
256
+
257
+ start_time = time.perf_counter()
258
+
259
+ categories_list: list[str]
260
+ flag_service = get_flag_service()
261
+ if flag_service.is_enabled("unified_manifest"):
262
+ all_tools = _build_unified_manifest_tools()
263
+ if category:
264
+ all_tools = [tool for tool in all_tools if tool.get("category") == category]
265
+ if tag:
266
+ all_tools = [tool for tool in all_tools if tag in (tool.get("tags") or [])]
267
+ categories_list = sorted(
268
+ {tool.get("category", "general") for tool in all_tools}
269
+ )
270
+ else:
271
+ registry = get_tool_registry()
272
+ all_tools = registry.list_tools(
273
+ category=category,
274
+ tag=tag,
275
+ include_deprecated=include_deprecated,
276
+ )
277
+ categories = registry.list_categories()
278
+ categories_list = [c["name"] for c in categories]
279
+
280
+ start_idx = 0
281
+ if cursor:
282
+ try:
283
+ cursor_data = decode_cursor(cursor)
284
+ start_idx = int(cursor_data.get("offset", 0))
285
+ except (CursorError, ValueError, TypeError) as exc:
286
+ return asdict(
287
+ error_response(
288
+ f"Invalid cursor: {exc}",
289
+ error_code=ErrorCode.INVALID_FORMAT,
290
+ error_type=ErrorType.VALIDATION,
291
+ remediation="Use the cursor returned by server(action=tools)",
292
+ request_id=request_id,
293
+ )
294
+ )
295
+
296
+ end_idx = start_idx + limit_int
297
+ paginated_tools = all_tools[start_idx:end_idx]
298
+ has_more = end_idx < len(all_tools)
299
+ next_cursor = encode_cursor({"offset": end_idx}) if has_more else None
300
+
301
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
302
+ _metrics.timer(_metric("tools") + ".duration_ms", elapsed_ms)
303
+
304
+ response = paginated_response(
305
+ data={
306
+ "tools": paginated_tools,
307
+ "categories": categories_list,
308
+ "filters_applied": {
309
+ "category": category,
310
+ "tag": tag,
311
+ "include_deprecated": include_deprecated,
312
+ },
313
+ },
314
+ cursor=next_cursor,
315
+ has_more=has_more,
316
+ page_size=limit_int,
317
+ total_count=len(all_tools),
318
+ )
319
+ telemetry = response.setdefault("meta", {}).setdefault("telemetry", {})
320
+ telemetry["duration_ms"] = round(elapsed_ms, 2)
321
+
322
+ manifest_label = "unified"
323
+ manifest_tokens = _estimate_tokens(
324
+ json.dumps(all_tools, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
325
+ )
326
+ telemetry["manifest_tokens"] = manifest_tokens
327
+ telemetry["manifest_tool_count"] = len(all_tools)
328
+ telemetry["manifest_token_budget"] = MANIFEST_TOKEN_BUDGET
329
+ telemetry["manifest_token_budget_max"] = MANIFEST_TOKEN_BUDGET_MAX
330
+
331
+ warning_message: str | None = None
332
+ if manifest_tokens > MANIFEST_TOKEN_BUDGET_MAX:
333
+ warning_message = (
334
+ "Manifest token estimate "
335
+ f"{manifest_tokens} exceeds maximum budget {MANIFEST_TOKEN_BUDGET_MAX}; "
336
+ "clients may fail to load the manifest."
337
+ )
338
+ elif manifest_tokens > MANIFEST_TOKEN_BUDGET:
339
+ warning_message = (
340
+ "Manifest token estimate "
341
+ f"{manifest_tokens} exceeds budget {MANIFEST_TOKEN_BUDGET}; "
342
+ "trim tool/action metadata to reduce token load."
343
+ )
344
+
345
+ if warning_message:
346
+ meta = response.setdefault("meta", {})
347
+ warnings = meta.get("warnings")
348
+ if warnings is None:
349
+ warnings = []
350
+ elif not isinstance(warnings, list):
351
+ warnings = [str(warnings)]
352
+ warnings.append(warning_message)
353
+ meta["warnings"] = warnings
354
+
355
+ manager = get_observability_manager()
356
+ if manager.is_metrics_enabled():
357
+ exporter = manager.get_prometheus_exporter()
358
+ exporter.record_manifest_snapshot(
359
+ manifest=manifest_label,
360
+ tokens=manifest_tokens,
361
+ tool_count=len(all_tools),
362
+ )
363
+ exporter.record_feature_flag_state(
364
+ "unified_manifest", flag_service.is_enabled("unified_manifest")
365
+ )
366
+
367
+ response["meta"]["request_id"] = request_id
368
+ return response
369
+
370
+
371
+ def _handle_schema(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
372
+ request_id = _request_id()
373
+ tool_name = payload.get("tool_name")
374
+ if not isinstance(tool_name, str) or not tool_name.strip():
375
+ return _validation_error(
376
+ message="tool_name is required",
377
+ request_id=request_id,
378
+ remediation="Provide a tool name like 'spec'",
379
+ )
380
+
381
+ registry = get_tool_registry()
382
+ schema = registry.get_tool_schema(tool_name.strip())
383
+ if schema is None:
384
+ return asdict(
385
+ error_response(
386
+ f"Tool '{tool_name}' not found",
387
+ error_code=ErrorCode.NOT_FOUND,
388
+ error_type=ErrorType.NOT_FOUND,
389
+ remediation="Use server(action=tools) to list available tools",
390
+ request_id=request_id,
391
+ )
392
+ )
393
+
394
+ return asdict(success_response(data=schema, request_id=request_id))
395
+
396
+
397
+ def _handle_capabilities(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
398
+ request_id = _request_id()
399
+ try:
400
+ caps = get_capabilities()
401
+ return asdict(success_response(data=caps, request_id=request_id))
402
+ except Exception as exc:
403
+ logger.exception("Error getting capabilities")
404
+ return asdict(
405
+ error_response(
406
+ f"Failed to get capabilities: {exc}",
407
+ error_code=ErrorCode.INTERNAL_ERROR,
408
+ error_type=ErrorType.INTERNAL,
409
+ remediation="Check server logs",
410
+ request_id=request_id,
411
+ )
412
+ )
413
+
414
+
415
+ def _handle_context(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
416
+ request_id = _request_id()
417
+
418
+ include_llm_value = payload.get("include_llm", True)
419
+ if include_llm_value is not None and not isinstance(include_llm_value, bool):
420
+ return _validation_error(
421
+ message="include_llm must be a boolean",
422
+ request_id=request_id,
423
+ remediation="Provide include_llm=true|false",
424
+ )
425
+ include_llm = include_llm_value if isinstance(include_llm_value, bool) else True
426
+
427
+ include_workflow_value = payload.get("include_workflow", True)
428
+ if include_workflow_value is not None and not isinstance(
429
+ include_workflow_value, bool
430
+ ):
431
+ return _validation_error(
432
+ message="include_workflow must be a boolean",
433
+ request_id=request_id,
434
+ remediation="Provide include_workflow=true|false",
435
+ )
436
+ include_workflow = (
437
+ include_workflow_value if isinstance(include_workflow_value, bool) else True
438
+ )
439
+
440
+ include_workspace_value = payload.get("include_workspace", True)
441
+ if include_workspace_value is not None and not isinstance(
442
+ include_workspace_value, bool
443
+ ):
444
+ return _validation_error(
445
+ message="include_workspace must be a boolean",
446
+ request_id=request_id,
447
+ remediation="Provide include_workspace=true|false",
448
+ )
449
+ include_workspace = (
450
+ include_workspace_value if isinstance(include_workspace_value, bool) else True
451
+ )
452
+
453
+ include_capabilities_value = payload.get("include_capabilities", True)
454
+ if include_capabilities_value is not None and not isinstance(
455
+ include_capabilities_value, bool
456
+ ):
457
+ return _validation_error(
458
+ message="include_capabilities must be a boolean",
459
+ request_id=request_id,
460
+ remediation="Provide include_capabilities=true|false",
461
+ )
462
+ include_capabilities = (
463
+ include_capabilities_value
464
+ if isinstance(include_capabilities_value, bool)
465
+ else True
466
+ )
467
+
468
+ return build_server_context_response(
469
+ config,
470
+ include_llm=include_llm,
471
+ include_workflow=include_workflow,
472
+ include_workspace=include_workspace,
473
+ include_capabilities=include_capabilities,
474
+ request_id=_request_id(),
475
+ )
476
+
477
+
478
+ def _handle_llm_status(*, config: ServerConfig, payload: Dict[str, Any]) -> dict:
479
+ return build_llm_status_response(request_id=_request_id())
480
+
481
+
482
+ _ACTION_SUMMARY = {
483
+ "tools": "List available tools with filters and pagination.",
484
+ "schema": "Return schema metadata for a tool.",
485
+ "capabilities": "Return server capability negotiation metadata.",
486
+ "context": "Return server context (paths, config, capabilities).",
487
+ "llm-status": "Return downstream LLM provider configuration health.",
488
+ }
489
+
490
+
491
+ def _build_router() -> ActionRouter:
492
+ return ActionRouter(
493
+ tool_name="server",
494
+ actions=[
495
+ ActionDefinition(
496
+ name="tools", handler=_handle_tools, summary=_ACTION_SUMMARY["tools"]
497
+ ),
498
+ ActionDefinition(
499
+ name="schema", handler=_handle_schema, summary=_ACTION_SUMMARY["schema"]
500
+ ),
501
+ ActionDefinition(
502
+ name="capabilities",
503
+ handler=_handle_capabilities,
504
+ summary=_ACTION_SUMMARY["capabilities"],
505
+ ),
506
+ ActionDefinition(
507
+ name="context",
508
+ handler=_handle_context,
509
+ summary=_ACTION_SUMMARY["context"],
510
+ ),
511
+ ActionDefinition(
512
+ name="llm-status",
513
+ handler=_handle_llm_status,
514
+ summary=_ACTION_SUMMARY["llm-status"],
515
+ aliases=("llm_status",),
516
+ ),
517
+ ],
518
+ )
519
+
520
+
521
+ _SERVER_ROUTER = _build_router()
522
+
523
+
524
+ def _dispatch_server_action(
525
+ *, action: str, payload: Dict[str, Any], config: ServerConfig
526
+ ) -> dict:
527
+ try:
528
+ return _SERVER_ROUTER.dispatch(action, config=config, payload=payload)
529
+ except ActionRouterError as exc:
530
+ allowed = ", ".join(exc.allowed_actions)
531
+ request_id = _request_id()
532
+ return asdict(
533
+ error_response(
534
+ f"Unsupported server action '{action}'. Allowed actions: {allowed}",
535
+ error_code=ErrorCode.VALIDATION_ERROR,
536
+ error_type=ErrorType.VALIDATION,
537
+ remediation=f"Use one of: {allowed}",
538
+ request_id=request_id,
539
+ )
540
+ )
541
+
542
+
543
+ def register_unified_server_tool(mcp: FastMCP, config: ServerConfig) -> None:
544
+ """Register the consolidated server tool."""
545
+
546
+ @canonical_tool(mcp, canonical_name="server")
547
+ @mcp_tool(tool_name="server", emit_metrics=True, audit=False)
548
+ def server(
549
+ action: str,
550
+ tool_name: Optional[str] = None,
551
+ category: Optional[str] = None,
552
+ tag: Optional[str] = None,
553
+ include_deprecated: bool = False,
554
+ cursor: Optional[str] = None,
555
+ limit: int = DEFAULT_PAGE_SIZE,
556
+ include_llm: bool = True,
557
+ include_workflow: bool = True,
558
+ include_workspace: bool = True,
559
+ include_capabilities: bool = True,
560
+ ) -> dict:
561
+ payload: Dict[str, Any] = {
562
+ "tool_name": tool_name,
563
+ "category": category,
564
+ "tag": tag,
565
+ "include_deprecated": include_deprecated,
566
+ "cursor": cursor,
567
+ "limit": limit,
568
+ "include_llm": include_llm,
569
+ "include_workflow": include_workflow,
570
+ "include_workspace": include_workspace,
571
+ "include_capabilities": include_capabilities,
572
+ }
573
+ return _dispatch_server_action(action=action, payload=payload, config=config)
574
+
575
+ logger.debug("Registered unified server tool")
576
+
577
+
578
+ __all__ = [
579
+ "register_unified_server_tool",
580
+ ]