stravinsky 0.1.2__py3-none-any.whl → 0.2.7__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 stravinsky might be problematic. Click here for more details.

mcp_bridge/server.py CHANGED
@@ -1,17 +1,17 @@
1
1
  """
2
- Claude Superagent MCP Bridge Server
2
+ Stravinsky MCP Bridge Server - Zero-Import-Weight Architecture
3
3
 
4
- Main entry point for the MCP server that provides tools for:
5
- - OAuth-authenticated Gemini model invocation
6
- - OAuth-authenticated OpenAI model invocation
7
- - LSP tool proxies
8
- - Session management
9
-
10
- Run with: python -m mcp_bridge.server
4
+ Optimized for extremely fast startup and protocol compliance:
5
+ - Lazy-loads all tool implementations and dependencies.
6
+ - Minimal top-level imports.
7
+ - Robust crash logging to stderr and /tmp.
11
8
  """
12
9
 
10
+ import sys
11
+ import os
13
12
  import asyncio
14
13
  import logging
14
+ import time
15
15
  from typing import Any
16
16
 
17
17
  from mcp.server import Server
@@ -25,799 +25,337 @@ from mcp.types import (
25
25
  GetPromptResult,
26
26
  )
27
27
 
28
- from .auth.token_store import TokenStore
29
- from .tools.model_invoke import invoke_gemini, invoke_openai
30
- from .tools.code_search import lsp_diagnostics, ast_grep_search, ast_grep_replace, grep_search, glob_files
31
- from .tools.session_manager import list_sessions, read_session, search_sessions, get_session_info
32
- from .tools.skill_loader import list_skills, get_skill, create_skill
33
- from .tools.background_tasks import task_spawn, task_status, task_list
34
- from .tools.agent_manager import agent_spawn, agent_output, agent_cancel, agent_list, agent_progress
35
- from .tools.project_context import get_project_context, get_system_health
36
- from .tools.lsp import (
37
- lsp_hover,
38
- lsp_goto_definition,
39
- lsp_find_references,
40
- lsp_document_symbols,
41
- lsp_workspace_symbols,
42
- lsp_prepare_rename,
43
- lsp_rename,
44
- lsp_code_actions,
45
- lsp_servers,
46
- )
47
- from .prompts import stravinsky, delphi, dewey, explore, frontend, document_writer, multimodal
28
+ from . import __version__
48
29
 
49
- # Configure logging
50
- logging.basicConfig(level=logging.INFO)
51
- logger = logging.getLogger(__name__)
30
+ # --- CRITICAL: PROTOCOL HYGIENE ---
52
31
 
53
- # Initialize the MCP server
54
- server = Server("stravinsky")
55
-
56
- # Token store for OAuth tokens
57
- token_store = TokenStore()
32
+ # Configure logging to stderr explicitly to avoid protocol corruption
33
+ logging.basicConfig(
34
+ level=logging.INFO,
35
+ format='%(levelname)s:%(name)s:%(message)s',
36
+ stream=sys.stderr
37
+ )
38
+ logger = logging.getLogger(__name__)
58
39
 
40
+ # Pre-async crash logger
41
+ def install_emergency_logger():
42
+ def handle_exception(exc_type, exc_value, exc_traceback):
43
+ if issubclass(exc_type, KeyboardInterrupt):
44
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
45
+ return
46
+ logger.critical("FATAL PRE-STARTUP ERROR", exc_info=(exc_type, exc_value, exc_traceback))
47
+ try:
48
+ with open("/tmp/stravinsky_crash.log", "a") as f:
49
+ import traceback
50
+ f.write(f"\n--- CRASH AT {time.ctime()} ---\n")
51
+ traceback.print_exception(exc_type, exc_value, exc_traceback, file=f)
52
+ except:
53
+ pass
54
+
55
+ sys.excepthook = handle_exception
56
+
57
+ install_emergency_logger()
58
+
59
+ # --- SERVER INITIALIZATION ---
60
+
61
+ server = Server("stravinsky", version=__version__)
62
+
63
+ # Lazy-loaded systems
64
+ _token_store = None
65
+ _hook_manager = None
66
+
67
+ def get_token_store():
68
+ global _token_store
69
+ if _token_store is None:
70
+ from .auth.token_store import TokenStore
71
+ _token_store = TokenStore()
72
+ return _token_store
73
+
74
+ def get_hook_manager_lazy():
75
+ global _hook_manager
76
+ if _hook_manager is None:
77
+ from .hooks.manager import get_hook_manager
78
+ _hook_manager = get_hook_manager()
79
+ return _hook_manager
80
+
81
+ # --- MCP INTERFACE ---
59
82
 
60
83
  @server.list_tools()
61
84
  async def list_tools() -> list[Tool]:
62
- """List all available tools."""
63
- tools = [
64
- Tool(
65
- name="invoke_gemini",
66
- description=(
67
- "Invoke a Gemini model with the given prompt. "
68
- "Requires OAuth authentication with Google. "
69
- "Use this for tasks requiring Gemini's capabilities like "
70
- "frontend UI generation, documentation writing, or multimodal analysis."
71
- ),
72
- inputSchema={
73
- "type": "object",
74
- "properties": {
75
- "prompt": {
76
- "type": "string",
77
- "description": "The prompt to send to Gemini",
78
- },
79
- "model": {
80
- "type": "string",
81
- "description": "Gemini model to use (default: gemini-3-flash)",
82
- "default": "gemini-3-flash",
83
- },
84
- "temperature": {
85
- "type": "number",
86
- "description": "Sampling temperature (0.0-2.0)",
87
- "default": 0.7,
88
- },
89
- "max_tokens": {
90
- "type": "integer",
91
- "description": "Maximum tokens in response",
92
- "default": 4096,
93
- },
94
- "thinking_budget": {
95
- "type": "integer",
96
- "description": "Tokens reserved for internal reasoning (if model supports it)",
97
- "default": 0,
98
- },
99
- },
100
- "required": ["prompt"],
101
- },
102
- ),
103
- Tool(
104
- name="invoke_openai",
105
- description=(
106
- "Invoke an OpenAI model with the given prompt. "
107
- "Requires OAuth authentication with OpenAI. "
108
- "Use this for tasks requiring GPT capabilities like "
109
- "strategic advice, code review, or complex reasoning."
110
- ),
111
- inputSchema={
112
- "type": "object",
113
- "properties": {
114
- "prompt": {
115
- "type": "string",
116
- "description": "The prompt to send to OpenAI",
117
- },
118
- "model": {
119
- "type": "string",
120
- "description": "OpenAI model to use (default: gpt-5.2)",
121
- "default": "gpt-5.2",
122
- },
123
- "temperature": {
124
- "type": "number",
125
- "description": "Sampling temperature (0.0-2.0)",
126
- "default": 0.7,
127
- },
128
- "max_tokens": {
129
- "type": "integer",
130
- "description": "Maximum tokens in response",
131
- "default": 4096,
132
- },
133
- "thinking_budget": {
134
- "type": "integer",
135
- "description": "Tokens reserved for internal reasoning (e.g. gpt-5.2 / o1 / o3)",
136
- "default": 0,
137
- },
138
- },
139
- "required": ["prompt"],
140
- },
141
- ),
142
- Tool(
143
- name="get_project_context",
144
- description="Summarize project environment including Git status, local rules (.claude/rules/), and pending todos.",
145
- inputSchema={
146
- "type": "object",
147
- "properties": {
148
- "project_path": {"type": "string", "description": "Path to the project root"},
149
- },
150
- },
151
- ),
152
- Tool(
153
- name="get_system_health",
154
- description="Comprehensive check of system dependencies (rg, fd, sg, etc.) and authentication status.",
155
- inputSchema={
156
- "type": "object",
157
- "properties": {},
158
- },
159
- ),
160
- Tool(
161
- name="lsp_diagnostics",
162
- description="Get diagnostics (errors, warnings) for a file using language tools (tsc, ruff).",
163
- inputSchema={
164
- "type": "object",
165
- "properties": {
166
- "file_path": {"type": "string", "description": "Path to file to analyze"},
167
- "severity": {"type": "string", "description": "Filter: error, warning, all", "default": "all"},
168
- },
169
- "required": ["file_path"],
170
- },
171
- ),
172
- Tool(
173
- name="ast_grep_search",
174
- description="Search codebase using ast-grep for structural AST patterns.",
175
- inputSchema={
176
- "type": "object",
177
- "properties": {
178
- "pattern": {"type": "string", "description": "ast-grep pattern"},
179
- "directory": {"type": "string", "description": "Directory to search", "default": "."},
180
- "language": {"type": "string", "description": "Filter by language"},
181
- },
182
- "required": ["pattern"],
183
- },
184
- ),
185
- Tool(
186
- name="grep_search",
187
- description="Fast text search using ripgrep.",
188
- inputSchema={
189
- "type": "object",
190
- "properties": {
191
- "pattern": {"type": "string", "description": "Search pattern (regex)"},
192
- "directory": {"type": "string", "description": "Directory to search", "default": "."},
193
- "file_pattern": {"type": "string", "description": "Glob filter (e.g. *.py)"},
194
- },
195
- "required": ["pattern"],
196
- },
197
- ),
198
- Tool(
199
- name="glob_files",
200
- description="Find files matching a glob pattern.",
201
- inputSchema={
202
- "type": "object",
203
- "properties": {
204
- "pattern": {"type": "string", "description": "Glob pattern (e.g. **/*.py)"},
205
- "directory": {"type": "string", "description": "Base directory", "default": "."},
206
- },
207
- "required": ["pattern"],
208
- },
209
- ),
210
- Tool(
211
- name="session_list",
212
- description="List Claude Code sessions with optional filtering.",
213
- inputSchema={
214
- "type": "object",
215
- "properties": {
216
- "project_path": {"type": "string", "description": "Filter by project path"},
217
- "limit": {"type": "integer", "description": "Max sessions", "default": 20},
218
- },
219
- },
220
- ),
221
- Tool(
222
- name="session_read",
223
- description="Read messages from a Claude Code session.",
224
- inputSchema={
225
- "type": "object",
226
- "properties": {
227
- "session_id": {"type": "string", "description": "Session ID"},
228
- "limit": {"type": "integer", "description": "Max messages"},
229
- },
230
- "required": ["session_id"],
231
- },
232
- ),
233
- Tool(
234
- name="session_search",
235
- description="Search across Claude Code session messages.",
236
- inputSchema={
237
- "type": "object",
238
- "properties": {
239
- "query": {"type": "string", "description": "Search query"},
240
- "session_id": {"type": "string", "description": "Search in specific session"},
241
- "limit": {"type": "integer", "description": "Max results", "default": 20},
242
- },
243
- "required": ["query"],
244
- },
245
- ),
246
- Tool(
247
- name="skill_list",
248
- description="List available Claude Code skills/commands from .claude/commands/.",
249
- inputSchema={
250
- "type": "object",
251
- "properties": {
252
- "project_path": {"type": "string", "description": "Project directory"},
253
- },
254
- },
255
- ),
256
- Tool(
257
- name="skill_get",
258
- description="Get the content of a specific skill/command.",
259
- inputSchema={
260
- "type": "object",
261
- "properties": {
262
- "name": {"type": "string", "description": "Skill name"},
263
- "project_path": {"type": "string", "description": "Project directory"},
264
- },
265
- "required": ["name"],
266
- },
267
- ),
268
- Tool(
269
- name="task_spawn",
270
- description=(
271
- "Spawn a background task to execute a prompt asynchronously. "
272
- "Returns a Task ID. Best for deep research or parallel processing."
273
- ),
274
- inputSchema={
275
- "type": "object",
276
- "properties": {
277
- "prompt": {"type": "string", "description": "The prompt for the background agent"},
278
- "model": {
279
- "type": "string",
280
- "description": "Model to use (gemini-3-flash or gpt-5.2)",
281
- "default": "gemini-3-flash"
282
- },
283
- },
284
- "required": ["prompt"],
285
- },
286
- ),
287
- Tool(
288
- name="task_status",
289
- description="Check the status and retrieve results of a background task.",
290
- inputSchema={
291
- "type": "object",
292
- "properties": {
293
- "task_id": {"type": "string", "description": "The ID of the task to check"},
294
- },
295
- "required": ["task_id"],
296
- },
297
- ),
298
- Tool(
299
- name="task_list",
300
- description="List all active and recent background tasks.",
301
- inputSchema={
302
- "type": "object",
303
- "properties": {},
304
- },
305
- ),
306
- # New Agent Tools with Full Tool Access
307
- Tool(
308
- name="agent_spawn",
309
- description=(
310
- "Spawn a background agent. Uses Gemini by default for fast execution. "
311
- "Set model='claude' to use Claude Code CLI with full tool access."
312
- ),
313
- inputSchema={
314
- "type": "object",
315
- "properties": {
316
- "prompt": {"type": "string", "description": "The task for the agent to perform"},
317
- "agent_type": {
318
- "type": "string",
319
- "description": "Agent type: explore, dewey, frontend, delphi",
320
- "default": "explore",
321
- },
322
- "description": {"type": "string", "description": "Short description for status display"},
323
- "model": {
324
- "type": "string",
325
- "description": "Model: gemini-3-flash (default) or claude",
326
- "default": "gemini-3-flash",
327
- },
328
- "thinking_budget": {
329
- "type": "integer",
330
- "description": "Tokens reserved for internal reasoning (if model supports it)",
331
- "default": 0,
332
- },
333
- },
334
- "required": ["prompt"],
335
- },
336
- ),
337
- Tool(
338
- name="agent_output",
339
- description="Get output from a background agent. Use block=true to wait for completion.",
340
- inputSchema={
341
- "type": "object",
342
- "properties": {
343
- "task_id": {"type": "string", "description": "The agent task ID"},
344
- "block": {"type": "boolean", "description": "Wait for completion", "default": False},
345
- },
346
- "required": ["task_id"],
347
- },
348
- ),
349
- Tool(
350
- name="agent_cancel",
351
- description="Cancel a running background agent.",
352
- inputSchema={
353
- "type": "object",
354
- "properties": {
355
- "task_id": {"type": "string", "description": "The agent task ID to cancel"},
356
- },
357
- "required": ["task_id"],
358
- },
359
- ),
360
- Tool(
361
- name="agent_list",
362
- description="List all background agent tasks with their status.",
363
- inputSchema={
364
- "type": "object",
365
- "properties": {},
366
- },
367
- ),
368
- Tool(
369
- name="agent_progress",
370
- description="Get real-time progress from a running background agent. Shows recent output lines to monitor what the agent is doing.",
371
- inputSchema={
372
- "type": "object",
373
- "properties": {
374
- "task_id": {"type": "string", "description": "The agent task ID"},
375
- "lines": {"type": "integer", "description": "Number of recent lines to show", "default": 20},
376
- },
377
- "required": ["task_id"],
378
- },
379
- ),
380
- # LSP Tools
381
- Tool(
382
- name="lsp_hover",
383
- description="Get type info, documentation, and signature at a position in a file.",
384
- inputSchema={
385
- "type": "object",
386
- "properties": {
387
- "file_path": {"type": "string", "description": "Absolute path to the file"},
388
- "line": {"type": "integer", "description": "Line number (1-indexed)"},
389
- "character": {"type": "integer", "description": "Character position (0-indexed)"},
390
- },
391
- "required": ["file_path", "line", "character"],
392
- },
393
- ),
394
- Tool(
395
- name="lsp_goto_definition",
396
- description="Find where a symbol is defined. Jump to symbol definition.",
397
- inputSchema={
398
- "type": "object",
399
- "properties": {
400
- "file_path": {"type": "string", "description": "Absolute path to the file"},
401
- "line": {"type": "integer", "description": "Line number (1-indexed)"},
402
- "character": {"type": "integer", "description": "Character position (0-indexed)"},
403
- },
404
- "required": ["file_path", "line", "character"],
405
- },
406
- ),
407
- Tool(
408
- name="lsp_find_references",
409
- description="Find all references to a symbol across the workspace.",
410
- inputSchema={
411
- "type": "object",
412
- "properties": {
413
- "file_path": {"type": "string", "description": "Absolute path to the file"},
414
- "line": {"type": "integer", "description": "Line number (1-indexed)"},
415
- "character": {"type": "integer", "description": "Character position (0-indexed)"},
416
- "include_declaration": {"type": "boolean", "description": "Include the declaration itself", "default": True},
417
- },
418
- "required": ["file_path", "line", "character"],
419
- },
420
- ),
421
- Tool(
422
- name="lsp_document_symbols",
423
- description="Get hierarchical outline of all symbols (functions, classes, methods) in a file.",
424
- inputSchema={
425
- "type": "object",
426
- "properties": {
427
- "file_path": {"type": "string", "description": "Absolute path to the file"},
428
- },
429
- "required": ["file_path"],
430
- },
431
- ),
432
- Tool(
433
- name="lsp_workspace_symbols",
434
- description="Search for symbols by name across the entire workspace.",
435
- inputSchema={
436
- "type": "object",
437
- "properties": {
438
- "query": {"type": "string", "description": "Symbol name to search for (fuzzy match)"},
439
- "directory": {"type": "string", "description": "Workspace directory", "default": "."},
440
- },
441
- "required": ["query"],
442
- },
443
- ),
444
- Tool(
445
- name="lsp_prepare_rename",
446
- description="Check if a symbol at position can be renamed. Use before lsp_rename.",
447
- inputSchema={
448
- "type": "object",
449
- "properties": {
450
- "file_path": {"type": "string", "description": "Absolute path to the file"},
451
- "line": {"type": "integer", "description": "Line number (1-indexed)"},
452
- "character": {"type": "integer", "description": "Character position (0-indexed)"},
453
- },
454
- "required": ["file_path", "line", "character"],
455
- },
456
- ),
457
- Tool(
458
- name="lsp_rename",
459
- description="Rename a symbol across the workspace. Use lsp_prepare_rename first to validate.",
460
- inputSchema={
461
- "type": "object",
462
- "properties": {
463
- "file_path": {"type": "string", "description": "Absolute path to the file"},
464
- "line": {"type": "integer", "description": "Line number (1-indexed)"},
465
- "character": {"type": "integer", "description": "Character position (0-indexed)"},
466
- "new_name": {"type": "string", "description": "New name for the symbol"},
467
- "dry_run": {"type": "boolean", "description": "Preview changes without applying", "default": True},
468
- },
469
- "required": ["file_path", "line", "character", "new_name"],
470
- },
471
- ),
472
- Tool(
473
- name="lsp_code_actions",
474
- description="Get available quick fixes and refactorings at a position.",
475
- inputSchema={
476
- "type": "object",
477
- "properties": {
478
- "file_path": {"type": "string", "description": "Absolute path to the file"},
479
- "line": {"type": "integer", "description": "Line number (1-indexed)"},
480
- "character": {"type": "integer", "description": "Character position (0-indexed)"},
481
- },
482
- "required": ["file_path", "line", "character"],
483
- },
484
- ),
485
- Tool(
486
- name="lsp_servers",
487
- description="List available LSP servers and their installation status.",
488
- inputSchema={
489
- "type": "object",
490
- "properties": {},
491
- },
492
- ),
493
- Tool(
494
- name="ast_grep_replace",
495
- description="Replace code patterns using ast-grep's AST-aware replacement. More reliable than text-based replace for refactoring.",
496
- inputSchema={
497
- "type": "object",
498
- "properties": {
499
- "pattern": {"type": "string", "description": "ast-grep pattern to search (e.g., 'console.log($A)')"},
500
- "replacement": {"type": "string", "description": "Replacement pattern (e.g., 'logger.debug($A)')"},
501
- "directory": {"type": "string", "description": "Directory to search in", "default": "."},
502
- "language": {"type": "string", "description": "Filter by language (typescript, python, etc.)"},
503
- "dry_run": {"type": "boolean", "description": "Preview changes without applying", "default": True},
504
- },
505
- "required": ["pattern", "replacement"],
506
- },
507
- ),
508
- ]
509
- return tools
510
-
85
+ """List available tools (metadata only)."""
86
+ from .server_tools import get_tool_definitions
87
+ return get_tool_definitions()
511
88
 
512
89
  @server.call_tool()
513
90
  async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
514
- """Handle tool invocations."""
515
- logger.info(f"Tool called: {name} with args: {arguments}")
91
+ """Handle tool calls with deep lazy loading of implementations."""
92
+ logger.info(f"Tool call: {name}")
93
+ hook_manager = get_hook_manager_lazy()
94
+ token_store = get_token_store()
516
95
 
517
96
  try:
97
+ # Pre-tool call hooks orchestration
98
+ arguments = await hook_manager.execute_pre_tool_call(name, arguments)
99
+
100
+ result_content = None
101
+
102
+ # --- MODEL DISPATCH ---
518
103
  if name == "invoke_gemini":
519
- result = await invoke_gemini(
104
+ from .tools.model_invoke import invoke_gemini
105
+ result_content = await invoke_gemini(
520
106
  token_store=token_store,
521
107
  prompt=arguments["prompt"],
522
- model=arguments.get("model", "gemini-3-flash"),
108
+ model=arguments.get("model", "gemini-2.0-flash-exp"),
523
109
  temperature=arguments.get("temperature", 0.7),
524
- max_tokens=arguments.get("max_tokens", 4096),
110
+ max_tokens=arguments.get("max_tokens", 8192),
525
111
  thinking_budget=arguments.get("thinking_budget", 0),
526
112
  )
527
- return [TextContent(type="text", text=result)]
528
113
 
529
114
  elif name == "invoke_openai":
530
- result = await invoke_openai(
115
+ from .tools.model_invoke import invoke_openai
116
+ result_content = await invoke_openai(
531
117
  token_store=token_store,
532
118
  prompt=arguments["prompt"],
533
- model=arguments.get("model", "gpt-5.2"),
119
+ model=arguments.get("model", "gpt-4o"),
534
120
  temperature=arguments.get("temperature", 0.7),
535
121
  max_tokens=arguments.get("max_tokens", 4096),
536
122
  thinking_budget=arguments.get("thinking_budget", 0),
537
123
  )
538
- return [TextContent(type="text", text=result)]
539
124
 
125
+ # --- CONTEXT DISPATCH ---
540
126
  elif name == "get_project_context":
541
- result = await get_project_context(
542
- project_path=arguments.get("project_path"),
543
- )
544
- return [TextContent(type="text", text=result)]
127
+ from .tools.project_context import get_project_context
128
+ result_content = await get_project_context(project_path=arguments.get("project_path"))
545
129
 
546
130
  elif name == "get_system_health":
547
- result = await get_system_health()
548
- return [TextContent(type="text", text=result)]
131
+ from .tools.project_context import get_system_health
132
+ result_content = await get_system_health()
549
133
 
550
- elif name == "lsp_diagnostics":
551
- result = await lsp_diagnostics(
552
- file_path=arguments["file_path"],
553
- severity=arguments.get("severity", "all"),
134
+ # --- SEARCH DISPATCH ---
135
+ elif name == "grep_search":
136
+ from .tools.code_search import grep_search
137
+ result_content = await grep_search(
138
+ pattern=arguments["pattern"],
139
+ directory=arguments.get("directory", "."),
140
+ file_pattern=arguments.get("file_pattern", ""),
554
141
  )
555
- return [TextContent(type="text", text=result)]
556
142
 
557
143
  elif name == "ast_grep_search":
558
- result = await ast_grep_search(
144
+ from .tools.code_search import ast_grep_search
145
+ result_content = await ast_grep_search(
559
146
  pattern=arguments["pattern"],
560
147
  directory=arguments.get("directory", "."),
561
148
  language=arguments.get("language", ""),
562
149
  )
563
- return [TextContent(type="text", text=result)]
564
150
 
565
- elif name == "grep_search":
566
- result = await grep_search(
151
+ elif name == "ast_grep_replace":
152
+ from .tools.code_search import ast_grep_replace
153
+ result_content = await ast_grep_replace(
567
154
  pattern=arguments["pattern"],
155
+ replacement=arguments["replacement"],
568
156
  directory=arguments.get("directory", "."),
569
- file_pattern=arguments.get("file_pattern", ""),
157
+ language=arguments.get("language", ""),
158
+ dry_run=arguments.get("dry_run", True),
570
159
  )
571
- return [TextContent(type="text", text=result)]
572
160
 
573
161
  elif name == "glob_files":
574
- result = await glob_files(
162
+ from .tools.code_search import glob_files
163
+ result_content = await glob_files(
575
164
  pattern=arguments["pattern"],
576
165
  directory=arguments.get("directory", "."),
577
166
  )
578
- return [TextContent(type="text", text=result)]
579
167
 
168
+ # --- SESSION DISPATCH ---
580
169
  elif name == "session_list":
581
- result = list_sessions(
170
+ from .tools.session_manager import list_sessions
171
+ result_content = list_sessions(
582
172
  project_path=arguments.get("project_path"),
583
173
  limit=arguments.get("limit", 20),
584
174
  )
585
- return [TextContent(type="text", text=result)]
586
175
 
587
176
  elif name == "session_read":
588
- result = read_session(
177
+ from .tools.session_manager import read_session
178
+ result_content = read_session(
589
179
  session_id=arguments["session_id"],
590
180
  limit=arguments.get("limit"),
591
181
  )
592
- return [TextContent(type="text", text=result)]
593
182
 
594
183
  elif name == "session_search":
595
- result = search_sessions(
184
+ from .tools.session_manager import search_sessions
185
+ result_content = search_sessions(
596
186
  query=arguments["query"],
597
187
  session_id=arguments.get("session_id"),
598
188
  limit=arguments.get("limit", 20),
599
189
  )
600
- return [TextContent(type="text", text=result)]
601
190
 
191
+ # --- SKILL DISPATCH ---
602
192
  elif name == "skill_list":
603
- result = list_skills(
604
- project_path=arguments.get("project_path"),
605
- )
606
- return [TextContent(type="text", text=result)]
193
+ from .tools.skill_loader import list_skills
194
+ result_content = list_skills(project_path=arguments.get("project_path"))
607
195
 
608
196
  elif name == "skill_get":
609
- result = get_skill(
197
+ from .tools.skill_loader import get_skill
198
+ result_content = get_skill(
610
199
  name=arguments["name"],
611
200
  project_path=arguments.get("project_path"),
612
201
  )
613
- return [TextContent(type="text", text=result)]
614
-
615
- elif name == "task_spawn":
616
- result = await task_spawn(
617
- prompt=arguments["prompt"],
618
- model=arguments.get("model", "gemini-3-flash"),
619
- )
620
- return [TextContent(type="text", text=result)]
621
202
 
622
- elif name == "task_status":
623
- result = await task_status(
624
- task_id=arguments["task_id"],
625
- )
626
- return [TextContent(type="text", text=result)]
627
-
628
- elif name == "task_list":
629
- result = await task_list()
630
- return [TextContent(type="text", text=result)]
631
-
632
- # Agent tools with full tool access
203
+ # --- AGENT DISPATCH ---
633
204
  elif name == "agent_spawn":
634
- result = await agent_spawn(
205
+ from .tools.agent_manager import get_agent_manager
206
+ manager = get_agent_manager()
207
+ result_content = await manager.spawn(
208
+ token_store=token_store,
635
209
  prompt=arguments["prompt"],
636
210
  agent_type=arguments.get("agent_type", "explore"),
637
211
  description=arguments.get("description", ""),
212
+ parent_session_id=arguments.get("parent_session_id"),
213
+ system_prompt=arguments.get("system_prompt"),
638
214
  model=arguments.get("model", "gemini-3-flash"),
639
215
  thinking_budget=arguments.get("thinking_budget", 0),
216
+ timeout=arguments.get("timeout", 300),
640
217
  )
641
- return [TextContent(type="text", text=result)]
642
218
 
643
219
  elif name == "agent_output":
644
- result = await agent_output(
220
+ from .tools.agent_manager import agent_output
221
+ result_content = await agent_output(
645
222
  task_id=arguments["task_id"],
646
223
  block=arguments.get("block", False),
647
224
  )
648
- return [TextContent(type="text", text=result)]
649
225
 
650
226
  elif name == "agent_cancel":
651
- result = await agent_cancel(
652
- task_id=arguments["task_id"],
653
- )
654
- return [TextContent(type="text", text=result)]
227
+ from .tools.agent_manager import agent_cancel
228
+ result_content = await agent_cancel(task_id=arguments["task_id"])
655
229
 
656
230
  elif name == "agent_list":
657
- result = await agent_list()
658
- return [TextContent(type="text", text=result)]
231
+ from .tools.agent_manager import agent_list
232
+ result_content = await agent_list()
659
233
 
660
234
  elif name == "agent_progress":
661
- result = await agent_progress(
235
+ from .tools.agent_manager import agent_progress
236
+ result_content = await agent_progress(
662
237
  task_id=arguments["task_id"],
663
238
  lines=arguments.get("lines", 20),
664
239
  )
665
- return [TextContent(type="text", text=result)]
666
240
 
667
- # LSP Tools
241
+ elif name == "agent_retry":
242
+ from .tools.agent_manager import agent_retry
243
+ result_content = await agent_retry(
244
+ task_id=arguments["task_id"],
245
+ new_prompt=arguments.get("new_prompt"),
246
+ new_timeout=arguments.get("new_timeout"),
247
+ )
248
+
249
+ # --- BACKGROUND TASK DISPATCH ---
250
+ elif name == "task_spawn":
251
+ from .tools.background_tasks import task_spawn
252
+ result_content = await task_spawn(
253
+ prompt=arguments["prompt"],
254
+ model=arguments.get("model", "gemini-3-flash"),
255
+ )
256
+
257
+ elif name == "task_status":
258
+ from .tools.background_tasks import task_status
259
+ result_content = await task_status(task_id=arguments["task_id"])
260
+
261
+ elif name == "task_list":
262
+ from .tools.background_tasks import task_list
263
+ result_content = await task_list()
264
+
265
+ # --- LSP DISPATCH ---
668
266
  elif name == "lsp_hover":
669
- result = await lsp_hover(
267
+ from .tools.lsp import lsp_hover
268
+ result_content = await lsp_hover(
670
269
  file_path=arguments["file_path"],
671
270
  line=arguments["line"],
672
271
  character=arguments["character"],
673
272
  )
674
- return [TextContent(type="text", text=result)]
675
273
 
676
274
  elif name == "lsp_goto_definition":
677
- result = await lsp_goto_definition(
275
+ from .tools.lsp import lsp_goto_definition
276
+ result_content = await lsp_goto_definition(
678
277
  file_path=arguments["file_path"],
679
278
  line=arguments["line"],
680
279
  character=arguments["character"],
681
280
  )
682
- return [TextContent(type="text", text=result)]
683
281
 
684
282
  elif name == "lsp_find_references":
685
- result = await lsp_find_references(
283
+ from .tools.lsp import lsp_find_references
284
+ result_content = await lsp_find_references(
686
285
  file_path=arguments["file_path"],
687
286
  line=arguments["line"],
688
287
  character=arguments["character"],
689
288
  include_declaration=arguments.get("include_declaration", True),
690
289
  )
691
- return [TextContent(type="text", text=result)]
692
290
 
693
291
  elif name == "lsp_document_symbols":
694
- result = await lsp_document_symbols(
695
- file_path=arguments["file_path"],
696
- )
697
- return [TextContent(type="text", text=result)]
292
+ from .tools.lsp import lsp_document_symbols
293
+ result_content = await lsp_document_symbols(file_path=arguments["file_path"])
698
294
 
699
295
  elif name == "lsp_workspace_symbols":
700
- result = await lsp_workspace_symbols(
701
- query=arguments["query"],
702
- directory=arguments.get("directory", "."),
703
- )
704
- return [TextContent(type="text", text=result)]
296
+ from .tools.lsp import lsp_workspace_symbols
297
+ result_content = await lsp_workspace_symbols(query=arguments["query"])
705
298
 
706
299
  elif name == "lsp_prepare_rename":
707
- result = await lsp_prepare_rename(
300
+ from .tools.lsp import lsp_prepare_rename
301
+ result_content = await lsp_prepare_rename(
708
302
  file_path=arguments["file_path"],
709
303
  line=arguments["line"],
710
304
  character=arguments["character"],
711
305
  )
712
- return [TextContent(type="text", text=result)]
713
306
 
714
307
  elif name == "lsp_rename":
715
- result = await lsp_rename(
308
+ from .tools.lsp import lsp_rename
309
+ result_content = await lsp_rename(
716
310
  file_path=arguments["file_path"],
717
311
  line=arguments["line"],
718
312
  character=arguments["character"],
719
313
  new_name=arguments["new_name"],
720
- dry_run=arguments.get("dry_run", True),
721
314
  )
722
- return [TextContent(type="text", text=result)]
723
315
 
724
316
  elif name == "lsp_code_actions":
725
- result = await lsp_code_actions(
317
+ from .tools.lsp import lsp_code_actions
318
+ result_content = await lsp_code_actions(
726
319
  file_path=arguments["file_path"],
727
320
  line=arguments["line"],
728
321
  character=arguments["character"],
729
322
  )
730
- return [TextContent(type="text", text=result)]
731
323
 
732
324
  elif name == "lsp_servers":
733
- result = await lsp_servers()
734
- return [TextContent(type="text", text=result)]
735
-
736
- elif name == "ast_grep_replace":
737
- result = await ast_grep_replace(
738
- pattern=arguments["pattern"],
739
- replacement=arguments["replacement"],
740
- directory=arguments.get("directory", "."),
741
- language=arguments.get("language", ""),
742
- dry_run=arguments.get("dry_run", True),
743
- )
744
- return [TextContent(type="text", text=result)]
325
+ from .tools.lsp import lsp_servers
326
+ result_content = await lsp_servers()
745
327
 
746
328
  else:
747
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
329
+ result_content = f"Unknown tool: {name}"
330
+
331
+ # Post-tool call hooks orchestration
332
+ if result_content is not None:
333
+ if isinstance(result_content, list) and len(result_content) > 0 and hasattr(result_content[0], "text"):
334
+ processed_text = await hook_manager.execute_post_tool_call(name, arguments, result_content[0].text)
335
+ result_content[0].text = processed_text
336
+ elif isinstance(result_content, str):
337
+ result_content = await hook_manager.execute_post_tool_call(name, arguments, result_content)
338
+
339
+ # Format final return as List[TextContent]
340
+ if isinstance(result_content, list):
341
+ return result_content
342
+ return [TextContent(type="text", text=str(result_content))]
748
343
 
749
344
  except Exception as e:
750
- logger.error(f"Error in tool {name}: {e}")
345
+ logger.error(f"Error calling tool {name}: {e}")
751
346
  return [TextContent(type="text", text=f"Error: {str(e)}")]
752
347
 
753
-
754
348
  @server.list_prompts()
755
349
  async def list_prompts() -> list[Prompt]:
756
- """List available agent prompts."""
757
- return [
758
- Prompt(
759
- name="stravinsky",
760
- description=(
761
- "Stravinsky - Powerful AI orchestrator. "
762
- "Plans obsessively with todos, assesses search complexity before "
763
- "exploration, delegates strategically to specialized agents."
764
- ),
765
- arguments=[],
766
- ),
767
- Prompt(
768
- name="delphi",
769
- description=(
770
- "Delphi - Strategic advisor using GPT for debugging, "
771
- "architecture review, and complex problem solving."
772
- ),
773
- arguments=[],
774
- ),
775
- Prompt(
776
- name="dewey",
777
- description=(
778
- "Dewey - Documentation and GitHub research specialist. "
779
- "Finds implementation examples, official docs, and code patterns."
780
- ),
781
- arguments=[],
782
- ),
783
- Prompt(
784
- name="explore",
785
- description=(
786
- "Explore - Fast codebase search specialist. "
787
- "Answers 'Where is X?', finds files and code patterns."
788
- ),
789
- arguments=[],
790
- ),
791
- Prompt(
792
- name="frontend",
793
- description=(
794
- "Frontend UI/UX Engineer - Designer-turned-developer for stunning visuals. "
795
- "Excels at styling, layout, animation, typography."
796
- ),
797
- arguments=[],
798
- ),
799
- Prompt(
800
- name="document_writer",
801
- description=(
802
- "Document Writer - Technical documentation specialist. "
803
- "README files, API docs, architecture docs, user guides."
804
- ),
805
- arguments=[],
806
- ),
807
- Prompt(
808
- name="multimodal",
809
- description=(
810
- "Multimodal Looker - Visual content analysis. "
811
- "PDFs, images, diagrams - extracts and interprets visual data."
812
- ),
813
- arguments=[],
814
- ),
815
- ]
816
-
350
+ """List available prompts (metadata only)."""
351
+ from .server_tools import get_prompt_definitions
352
+ return get_prompt_definitions()
817
353
 
818
354
  @server.get_prompt()
819
355
  async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptResult:
820
- """Get a specific agent prompt."""
356
+ """Get a specific prompt content (lazy loaded)."""
357
+ from .prompts import stravinsky, delphi, dewey, explore, frontend, document_writer, multimodal
358
+
821
359
  prompts_map = {
822
360
  "stravinsky": ("Stravinsky orchestrator system prompt", stravinsky.get_stravinsky_prompt),
823
361
  "delphi": ("Delphi advisor system prompt", delphi.get_delphi_prompt),
@@ -844,23 +382,42 @@ async def get_prompt(name: str, arguments: dict[str, str] | None) -> GetPromptRe
844
382
  ],
845
383
  )
846
384
 
847
-
848
385
  async def async_main():
849
- """Async entry point for the MCP server."""
850
- logger.info("Starting Stravinsky MCP Bridge Server...")
851
-
852
- async with stdio_server() as (read_stream, write_stream):
853
- await server.run(
854
- read_stream,
855
- write_stream,
856
- server.create_initialization_options(),
857
- )
386
+ """Server execution entry point."""
387
+ # Initialize hooks at runtime, not import time
388
+ try:
389
+ from .hooks import initialize_hooks
390
+ initialize_hooks()
391
+ except Exception as e:
392
+ logger.error(f"Failed to initialize hooks: {e}")
858
393
 
394
+ try:
395
+ async with stdio_server() as (read_stream, write_stream):
396
+ await server.run(
397
+ read_stream,
398
+ write_stream,
399
+ server.create_initialization_options(),
400
+ )
401
+ except Exception as e:
402
+ logger.critical("Server process crashed in async_main", exc_info=True)
403
+ sys.exit(1)
859
404
 
860
405
  def main():
861
- """Synchronous main entry point for uvx/CLI."""
862
- asyncio.run(async_main())
863
-
406
+ """Synchronous entry point with CLI arg handling."""
407
+ import argparse
408
+ parser = argparse.ArgumentParser(description="Stravinsky MCP Bridge Server")
409
+ parser.add_argument("--version", action="version", version=f"stravinsky {__version__}")
410
+
411
+ # Check for CLI flags before starting async loop
412
+ args, unknown = parser.parse_known_args()
413
+
414
+ try:
415
+ asyncio.run(async_main())
416
+ except KeyboardInterrupt:
417
+ pass
418
+ except Exception as e:
419
+ logger.critical(f"Unhandled exception in main: {e}")
420
+ sys.exit(1)
864
421
 
865
422
  if __name__ == "__main__":
866
423
  main()