foundry-mcp 0.8.22__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.

Potentially problematic release.


This version of foundry-mcp might be problematic. Click here for more details.

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