devrel-origin 0.2.14__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 (98) hide show
  1. devrel_origin/__init__.py +15 -0
  2. devrel_origin/cli/__init__.py +92 -0
  3. devrel_origin/cli/_common.py +243 -0
  4. devrel_origin/cli/analytics.py +28 -0
  5. devrel_origin/cli/argus.py +497 -0
  6. devrel_origin/cli/auth.py +227 -0
  7. devrel_origin/cli/config.py +108 -0
  8. devrel_origin/cli/content.py +259 -0
  9. devrel_origin/cli/cost.py +108 -0
  10. devrel_origin/cli/cro.py +298 -0
  11. devrel_origin/cli/deliverables.py +65 -0
  12. devrel_origin/cli/docs.py +91 -0
  13. devrel_origin/cli/doctor.py +178 -0
  14. devrel_origin/cli/experiment.py +29 -0
  15. devrel_origin/cli/growth.py +97 -0
  16. devrel_origin/cli/init.py +472 -0
  17. devrel_origin/cli/intel.py +27 -0
  18. devrel_origin/cli/kb.py +96 -0
  19. devrel_origin/cli/listen.py +31 -0
  20. devrel_origin/cli/marketing.py +66 -0
  21. devrel_origin/cli/migrate.py +45 -0
  22. devrel_origin/cli/run.py +46 -0
  23. devrel_origin/cli/sales.py +57 -0
  24. devrel_origin/cli/schedule.py +62 -0
  25. devrel_origin/cli/synthesize.py +28 -0
  26. devrel_origin/cli/triage.py +29 -0
  27. devrel_origin/cli/video.py +35 -0
  28. devrel_origin/core/__init__.py +58 -0
  29. devrel_origin/core/agent_config.py +75 -0
  30. devrel_origin/core/argus.py +964 -0
  31. devrel_origin/core/atlas.py +1450 -0
  32. devrel_origin/core/base.py +372 -0
  33. devrel_origin/core/cyra.py +563 -0
  34. devrel_origin/core/dex.py +708 -0
  35. devrel_origin/core/echo.py +614 -0
  36. devrel_origin/core/growth/__init__.py +27 -0
  37. devrel_origin/core/growth/recommendations.py +219 -0
  38. devrel_origin/core/growth/target_kinds.py +51 -0
  39. devrel_origin/core/iris.py +513 -0
  40. devrel_origin/core/kai.py +1367 -0
  41. devrel_origin/core/llm.py +542 -0
  42. devrel_origin/core/llm_backends.py +274 -0
  43. devrel_origin/core/mox.py +514 -0
  44. devrel_origin/core/nova.py +349 -0
  45. devrel_origin/core/pax.py +1205 -0
  46. devrel_origin/core/rex.py +532 -0
  47. devrel_origin/core/sage.py +486 -0
  48. devrel_origin/core/sentinel.py +385 -0
  49. devrel_origin/core/types.py +98 -0
  50. devrel_origin/core/video/__init__.py +22 -0
  51. devrel_origin/core/video/assembler.py +131 -0
  52. devrel_origin/core/video/browser_recorder.py +118 -0
  53. devrel_origin/core/video/desktop_recorder.py +254 -0
  54. devrel_origin/core/video/overlay_renderer.py +143 -0
  55. devrel_origin/core/video/script_parser.py +147 -0
  56. devrel_origin/core/video/tts_engine.py +82 -0
  57. devrel_origin/core/vox.py +268 -0
  58. devrel_origin/core/watchdog.py +321 -0
  59. devrel_origin/project/__init__.py +1 -0
  60. devrel_origin/project/config.py +75 -0
  61. devrel_origin/project/cost_sink.py +61 -0
  62. devrel_origin/project/init.py +104 -0
  63. devrel_origin/project/paths.py +75 -0
  64. devrel_origin/project/state.py +241 -0
  65. devrel_origin/project/templates/__init__.py +4 -0
  66. devrel_origin/project/templates/config.toml +24 -0
  67. devrel_origin/project/templates/devrel.gitignore +10 -0
  68. devrel_origin/project/templates/slop-blocklist.md +45 -0
  69. devrel_origin/project/templates/style.md +24 -0
  70. devrel_origin/project/templates/voice.md +29 -0
  71. devrel_origin/quality/__init__.py +66 -0
  72. devrel_origin/quality/editorial.py +357 -0
  73. devrel_origin/quality/persona.py +84 -0
  74. devrel_origin/quality/readability.py +148 -0
  75. devrel_origin/quality/slop.py +167 -0
  76. devrel_origin/quality/style.py +110 -0
  77. devrel_origin/quality/voice.py +15 -0
  78. devrel_origin/tools/__init__.py +9 -0
  79. devrel_origin/tools/analytics.py +304 -0
  80. devrel_origin/tools/api_client.py +393 -0
  81. devrel_origin/tools/apollo_client.py +305 -0
  82. devrel_origin/tools/code_validator.py +428 -0
  83. devrel_origin/tools/github_tools.py +297 -0
  84. devrel_origin/tools/instantly_client.py +412 -0
  85. devrel_origin/tools/kb_harvester.py +340 -0
  86. devrel_origin/tools/mcp_server.py +578 -0
  87. devrel_origin/tools/notifications.py +245 -0
  88. devrel_origin/tools/run_report.py +193 -0
  89. devrel_origin/tools/scheduler.py +231 -0
  90. devrel_origin/tools/search_tools.py +321 -0
  91. devrel_origin/tools/self_improve.py +168 -0
  92. devrel_origin/tools/sheets.py +236 -0
  93. devrel_origin-0.2.14.dist-info/METADATA +354 -0
  94. devrel_origin-0.2.14.dist-info/RECORD +98 -0
  95. devrel_origin-0.2.14.dist-info/WHEEL +5 -0
  96. devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
  97. devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
  98. devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
@@ -0,0 +1,578 @@
1
+ """
2
+ MCP Server — Model Context Protocol server exposing agent tools.
3
+
4
+ Registers all agent tools (PostHog API, GitHub, Search) as MCP-compatible
5
+ resources so external clients (Claude Desktop, IDE plugins) can invoke them.
6
+
7
+ Uses the standard MCP JSON-RPC transport over stdio.
8
+ """
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import sys
14
+ from dataclasses import asdict
15
+ from typing import Any, Callable, Coroutine, Optional
16
+
17
+ from devrel_origin.tools.api_client import InsightQuery, PostHogClient
18
+ from devrel_origin.tools.github_tools import GitHubTools
19
+ from devrel_origin.tools.search_tools import SearchTools
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Tool Registry
26
+ # ---------------------------------------------------------------------------
27
+
28
+ ToolHandler = Callable[..., Coroutine[Any, Any, Any]]
29
+
30
+
31
+ class ToolDefinition:
32
+ """Schema for a single MCP tool."""
33
+
34
+ def __init__(
35
+ self,
36
+ name: str,
37
+ description: str,
38
+ input_schema: dict[str, Any],
39
+ handler: ToolHandler,
40
+ ):
41
+ self.name = name
42
+ self.description = description
43
+ self.input_schema = input_schema
44
+ self.handler = handler
45
+
46
+ def to_manifest(self) -> dict[str, Any]:
47
+ return {
48
+ "name": self.name,
49
+ "description": self.description,
50
+ "inputSchema": self.input_schema,
51
+ }
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # MCP Server
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ class MCPServer:
60
+ """
61
+ Model Context Protocol server for the DevTools Advocate Agent.
62
+
63
+ Exposes agent tools over JSON-RPC stdio transport so that Claude Desktop,
64
+ IDE plugins, or other MCP clients can invoke them directly.
65
+
66
+ Usage::
67
+
68
+ server = MCPServer(
69
+ posthog_api_key="phx_...",
70
+ posthog_project_id="12345",
71
+ github_token="ghp_...",
72
+ firecrawl_api_key="fc-...",
73
+ )
74
+ await server.run() # Listens on stdin/stdout
75
+ """
76
+
77
+ SERVER_NAME = "devrel-origin"
78
+ SERVER_VERSION = "1.0.0"
79
+ PROTOCOL_VERSION = "2024-11-05"
80
+
81
+ def __init__(
82
+ self,
83
+ posthog_api_key: str = "",
84
+ posthog_project_id: str = "",
85
+ github_token: str = "",
86
+ firecrawl_api_key: str = "",
87
+ brave_api_key: str = "",
88
+ ):
89
+ # Initialize tool clients
90
+ self._posthog = (
91
+ PostHogClient(api_key=posthog_api_key, project_id=posthog_project_id)
92
+ if posthog_api_key
93
+ else None
94
+ )
95
+
96
+ self._github = GitHubTools(token=github_token) if github_token else None
97
+
98
+ self._search = SearchTools(
99
+ firecrawl_api_key=firecrawl_api_key,
100
+ brave_api_key=brave_api_key,
101
+ )
102
+
103
+ # Build tool registry
104
+ self._tools: dict[str, ToolDefinition] = {}
105
+ self._register_tools()
106
+
107
+ def _register_tools(self) -> None:
108
+ """Register all available tools with their schemas."""
109
+
110
+ # -- PostHog Tools --------------------------------------------------
111
+ if self._posthog:
112
+ self._tools["posthog_query_insights"] = ToolDefinition(
113
+ name="posthog_query_insights",
114
+ description=(
115
+ "Run a PostHog insight query (trends, funnels, retention, "
116
+ "paths, lifecycle). Returns time-series or aggregate data."
117
+ ),
118
+ input_schema={
119
+ "type": "object",
120
+ "properties": {
121
+ "insight": {
122
+ "type": "string",
123
+ "enum": ["TRENDS", "FUNNELS", "RETENTION", "PATHS", "LIFECYCLE"],
124
+ "description": "Type of insight to query",
125
+ },
126
+ "events": {
127
+ "type": "array",
128
+ "items": {"type": "object"},
129
+ "description": "Events to include, e.g. [{'id': '$pageview'}]",
130
+ },
131
+ "date_from": {
132
+ "type": "string",
133
+ "description": "Start date, e.g. '-7d' or '2024-01-01'",
134
+ "default": "-7d",
135
+ },
136
+ "date_to": {
137
+ "type": "string",
138
+ "description": "End date (optional)",
139
+ },
140
+ "interval": {
141
+ "type": "string",
142
+ "enum": ["hour", "day", "week", "month"],
143
+ "default": "day",
144
+ },
145
+ "breakdown": {
146
+ "type": "string",
147
+ "description": "Property to break down by (optional)",
148
+ },
149
+ },
150
+ "required": ["insight", "events"],
151
+ },
152
+ handler=self._handle_posthog_query,
153
+ )
154
+
155
+ self._tools["posthog_list_feature_flags"] = ToolDefinition(
156
+ name="posthog_list_feature_flags",
157
+ description="List all feature flags in the PostHog project.",
158
+ input_schema={
159
+ "type": "object",
160
+ "properties": {
161
+ "limit": {"type": "integer", "default": 100},
162
+ },
163
+ },
164
+ handler=self._handle_list_flags,
165
+ )
166
+
167
+ self._tools["posthog_list_experiments"] = ToolDefinition(
168
+ name="posthog_list_experiments",
169
+ description="List all experiments in the PostHog project.",
170
+ input_schema={
171
+ "type": "object",
172
+ "properties": {
173
+ "limit": {"type": "integer", "default": 100},
174
+ },
175
+ },
176
+ handler=self._handle_list_experiments,
177
+ )
178
+
179
+ self._tools["posthog_get_experiment_results"] = ToolDefinition(
180
+ name="posthog_get_experiment_results",
181
+ description="Fetch statistical results for a PostHog experiment.",
182
+ input_schema={
183
+ "type": "object",
184
+ "properties": {
185
+ "experiment_id": {"type": "integer", "description": "Experiment ID"},
186
+ },
187
+ "required": ["experiment_id"],
188
+ },
189
+ handler=self._handle_experiment_results,
190
+ )
191
+
192
+ self._tools["posthog_capture_event"] = ToolDefinition(
193
+ name="posthog_capture_event",
194
+ description="Capture a single analytics event in PostHog.",
195
+ input_schema={
196
+ "type": "object",
197
+ "properties": {
198
+ "distinct_id": {"type": "string"},
199
+ "event": {"type": "string"},
200
+ "properties": {"type": "object"},
201
+ },
202
+ "required": ["distinct_id", "event"],
203
+ },
204
+ handler=self._handle_capture_event,
205
+ )
206
+
207
+ # -- GitHub Tools ---------------------------------------------------
208
+ if self._github:
209
+ self._tools["github_fetch_recent_issues"] = ToolDefinition(
210
+ name="github_fetch_recent_issues",
211
+ description=(
212
+ "Fetch recent GitHub issues from the configured repository. "
213
+ "Useful for community triage and trend detection."
214
+ ),
215
+ input_schema={
216
+ "type": "object",
217
+ "properties": {
218
+ "days": {"type": "integer", "default": 7},
219
+ "state": {
220
+ "type": "string",
221
+ "enum": ["open", "closed", "all"],
222
+ "default": "open",
223
+ },
224
+ "labels": {
225
+ "type": "array",
226
+ "items": {"type": "string"},
227
+ "description": "Filter by label names",
228
+ },
229
+ },
230
+ },
231
+ handler=self._handle_fetch_issues,
232
+ )
233
+
234
+ self._tools["github_get_issue"] = ToolDefinition(
235
+ name="github_get_issue",
236
+ description="Fetch a single GitHub issue by number.",
237
+ input_schema={
238
+ "type": "object",
239
+ "properties": {
240
+ "issue_number": {"type": "integer"},
241
+ },
242
+ "required": ["issue_number"],
243
+ },
244
+ handler=self._handle_get_issue,
245
+ )
246
+
247
+ self._tools["github_search_similar_issues"] = ToolDefinition(
248
+ name="github_search_similar_issues",
249
+ description=(
250
+ "Search for GitHub issues matching a query. "
251
+ "Useful for duplicate detection and pattern finding."
252
+ ),
253
+ input_schema={
254
+ "type": "object",
255
+ "properties": {
256
+ "query": {"type": "string"},
257
+ "limit": {"type": "integer", "default": 5},
258
+ },
259
+ "required": ["query"],
260
+ },
261
+ handler=self._handle_search_issues,
262
+ )
263
+
264
+ self._tools["github_get_contributor_profile"] = ToolDefinition(
265
+ name="github_get_contributor_profile",
266
+ description="Get activity summary for a GitHub contributor.",
267
+ input_schema={
268
+ "type": "object",
269
+ "properties": {
270
+ "username": {"type": "string"},
271
+ },
272
+ "required": ["username"],
273
+ },
274
+ handler=self._handle_contributor_profile,
275
+ )
276
+
277
+ self._tools["github_repo_stats"] = ToolDefinition(
278
+ name="github_repo_stats",
279
+ description="Get repository statistics (stars, forks, open issues).",
280
+ input_schema={"type": "object", "properties": {}},
281
+ handler=self._handle_repo_stats,
282
+ )
283
+
284
+ # -- Search Tools ---------------------------------------------------
285
+ self._tools["search_devrel_ai_agents_docs"] = ToolDefinition(
286
+ name="search_devrel_ai_agents_docs",
287
+ description=(
288
+ "Search OpenClaw documentation for a topic. "
289
+ "Returns relevant doc pages with snippets."
290
+ ),
291
+ input_schema={
292
+ "type": "object",
293
+ "properties": {
294
+ "query": {"type": "string"},
295
+ "limit": {"type": "integer", "default": 10},
296
+ },
297
+ "required": ["query"],
298
+ },
299
+ handler=self._handle_search_docs,
300
+ )
301
+
302
+ self._tools["search_web"] = ToolDefinition(
303
+ name="search_web",
304
+ description=(
305
+ "General web search via Firecrawl API. "
306
+ "Useful for competitive analysis and trend research."
307
+ ),
308
+ input_schema={
309
+ "type": "object",
310
+ "properties": {
311
+ "query": {"type": "string"},
312
+ "limit": {"type": "integer", "default": 10},
313
+ },
314
+ "required": ["query"],
315
+ },
316
+ handler=self._handle_web_search,
317
+ )
318
+
319
+ self._tools["search_discourse"] = ToolDefinition(
320
+ name="search_discourse",
321
+ description="Search OpenClaw community forum.",
322
+ input_schema={
323
+ "type": "object",
324
+ "properties": {
325
+ "query": {"type": "string"},
326
+ "limit": {"type": "integer", "default": 10},
327
+ },
328
+ "required": ["query"],
329
+ },
330
+ handler=self._handle_search_discourse,
331
+ )
332
+
333
+ self._tools["fetch_url_content"] = ToolDefinition(
334
+ name="fetch_url_content",
335
+ description="Fetch and extract text content from a URL.",
336
+ input_schema={
337
+ "type": "object",
338
+ "properties": {
339
+ "url": {"type": "string"},
340
+ "max_chars": {"type": "integer", "default": 10000},
341
+ },
342
+ "required": ["url"],
343
+ },
344
+ handler=self._handle_fetch_url,
345
+ )
346
+
347
+ logger.info(f"Registered {len(self._tools)} MCP tools")
348
+
349
+ # -- Tool Handlers -------------------------------------------------------
350
+
351
+ async def _handle_posthog_query(self, **kwargs: Any) -> Any:
352
+ query = InsightQuery(
353
+ insight=kwargs.get("insight", "TRENDS"),
354
+ events=kwargs.get("events", []),
355
+ date_from=kwargs.get("date_from", "-7d"),
356
+ date_to=kwargs.get("date_to"),
357
+ interval=kwargs.get("interval", "day"),
358
+ breakdown=kwargs.get("breakdown"),
359
+ )
360
+ return await self._posthog.query_insights(query)
361
+
362
+ async def _handle_list_flags(self, **kwargs: Any) -> Any:
363
+ return await self._posthog.list_feature_flags(limit=kwargs.get("limit", 100))
364
+
365
+ async def _handle_list_experiments(self, **kwargs: Any) -> Any:
366
+ return await self._posthog.list_experiments(limit=kwargs.get("limit", 100))
367
+
368
+ async def _handle_experiment_results(self, **kwargs: Any) -> Any:
369
+ return await self._posthog.get_experiment_results(kwargs["experiment_id"])
370
+
371
+ async def _handle_capture_event(self, **kwargs: Any) -> Any:
372
+ return await self._posthog.capture(
373
+ distinct_id=kwargs["distinct_id"],
374
+ event=kwargs["event"],
375
+ properties=kwargs.get("properties", {}),
376
+ )
377
+
378
+ async def _handle_fetch_issues(self, **kwargs: Any) -> Any:
379
+ issues = await self._github.fetch_recent_issues(
380
+ days=kwargs.get("days", 7),
381
+ state=kwargs.get("state", "open"),
382
+ labels=kwargs.get("labels"),
383
+ )
384
+ return [asdict(i) for i in issues]
385
+
386
+ async def _handle_get_issue(self, **kwargs: Any) -> Any:
387
+ issue = await self._github.get_issue(kwargs["issue_number"])
388
+ return asdict(issue)
389
+
390
+ async def _handle_search_issues(self, **kwargs: Any) -> Any:
391
+ issues = await self._github.search_similar_issues(
392
+ query=kwargs["query"],
393
+ limit=kwargs.get("limit", 5),
394
+ )
395
+ return [asdict(i) for i in issues]
396
+
397
+ async def _handle_contributor_profile(self, **kwargs: Any) -> Any:
398
+ profile = await self._github.get_contributor_profile(kwargs["username"])
399
+ return asdict(profile)
400
+
401
+ async def _handle_repo_stats(self, **kwargs: Any) -> Any:
402
+ return await self._github.get_repo_stats()
403
+
404
+ async def _handle_search_docs(self, **kwargs: Any) -> Any:
405
+ results = await self._search.search_devrel_ai_agents_docs(
406
+ query=kwargs["query"],
407
+ limit=kwargs.get("limit", 10),
408
+ )
409
+ return [asdict(r) for r in results]
410
+
411
+ async def _handle_web_search(self, **kwargs: Any) -> Any:
412
+ results = await self._search.web_search(
413
+ query=kwargs["query"],
414
+ limit=kwargs.get("limit", 10),
415
+ )
416
+ return [asdict(r) for r in results]
417
+
418
+ async def _handle_search_discourse(self, **kwargs: Any) -> Any:
419
+ results = await self._search.search_discourse(
420
+ query=kwargs["query"],
421
+ limit=kwargs.get("limit", 10),
422
+ )
423
+ return [asdict(r) for r in results]
424
+
425
+ async def _handle_fetch_url(self, **kwargs: Any) -> Any:
426
+ content = await self._search.fetch_url_content(
427
+ url=kwargs["url"],
428
+ max_chars=kwargs.get("max_chars", 10000),
429
+ )
430
+ return {"content": content, "url": kwargs["url"]}
431
+
432
+ # -- JSON-RPC Transport --------------------------------------------------
433
+
434
+ async def run(self) -> None:
435
+ """Run the MCP server on stdio (JSON-RPC over stdin/stdout)."""
436
+ logger.info(f"Starting MCP server: {self.SERVER_NAME} v{self.SERVER_VERSION}")
437
+
438
+ loop = asyncio.get_running_loop()
439
+ reader = asyncio.StreamReader()
440
+ protocol = asyncio.StreamReaderProtocol(reader)
441
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin.buffer)
442
+
443
+ writer_transport, writer_protocol = await loop.connect_write_pipe(
444
+ asyncio.streams.FlowControlMixin, sys.stdout.buffer
445
+ )
446
+ writer = asyncio.StreamWriter(writer_transport, writer_protocol, None, loop)
447
+
448
+ try:
449
+ while True:
450
+ line = await reader.readline()
451
+ if not line:
452
+ break
453
+
454
+ try:
455
+ request = json.loads(line.decode())
456
+ response = await self._handle_request(request)
457
+ if response is not None:
458
+ writer.write((json.dumps(response) + "\n").encode())
459
+ await writer.drain()
460
+ except json.JSONDecodeError:
461
+ logger.warning("Invalid JSON received")
462
+ except Exception as exc:
463
+ logger.error(f"Error handling request: {exc}")
464
+ finally:
465
+ await self._cleanup()
466
+
467
+ async def _handle_request(self, request: dict[str, Any]) -> Optional[dict[str, Any]]:
468
+ """Route a JSON-RPC request to the appropriate handler."""
469
+ method = request.get("method", "")
470
+ req_id = request.get("id")
471
+ params = request.get("params", {})
472
+
473
+ if method == "initialize":
474
+ return self._rpc_response(
475
+ req_id,
476
+ {
477
+ "protocolVersion": self.PROTOCOL_VERSION,
478
+ "capabilities": {"tools": {"listChanged": False}},
479
+ "serverInfo": {
480
+ "name": self.SERVER_NAME,
481
+ "version": self.SERVER_VERSION,
482
+ },
483
+ },
484
+ )
485
+
486
+ elif method == "tools/list":
487
+ tools = [t.to_manifest() for t in self._tools.values()]
488
+ return self._rpc_response(req_id, {"tools": tools})
489
+
490
+ elif method == "tools/call":
491
+ tool_name = params.get("name", "")
492
+ arguments = params.get("arguments", {})
493
+
494
+ if tool_name not in self._tools:
495
+ return self._rpc_error(req_id, -32602, f"Unknown tool: {tool_name}")
496
+
497
+ try:
498
+ result = await self._tools[tool_name].handler(**arguments)
499
+ return self._rpc_response(
500
+ req_id, {"content": [{"type": "text", "text": json.dumps(result, default=str)}]}
501
+ )
502
+ except Exception as exc:
503
+ return self._rpc_response(
504
+ req_id,
505
+ {
506
+ "content": [{"type": "text", "text": f"Error: {exc}"}],
507
+ "isError": True,
508
+ },
509
+ )
510
+
511
+ elif method == "notifications/initialized":
512
+ return None # Notification, no response needed
513
+
514
+ else:
515
+ return self._rpc_error(req_id, -32601, f"Unknown method: {method}")
516
+
517
+ @staticmethod
518
+ def _rpc_response(req_id: Any, result: dict[str, Any]) -> dict[str, Any]:
519
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
520
+
521
+ @staticmethod
522
+ def _rpc_error(req_id: Any, code: int, message: str) -> dict[str, Any]:
523
+ return {
524
+ "jsonrpc": "2.0",
525
+ "id": req_id,
526
+ "error": {"code": code, "message": message},
527
+ }
528
+
529
+ async def _cleanup(self) -> None:
530
+ """Close all underlying HTTP clients."""
531
+ if self._posthog:
532
+ await self._posthog.close()
533
+ if self._github:
534
+ await self._github.close()
535
+ await self._search.close()
536
+
537
+
538
+ # ---------------------------------------------------------------------------
539
+ # CLI Entry Point
540
+ # ---------------------------------------------------------------------------
541
+
542
+
543
+ def main() -> None:
544
+ """Launch the MCP server from the command line."""
545
+ import argparse
546
+ import os
547
+
548
+ from dotenv import load_dotenv
549
+
550
+ load_dotenv()
551
+
552
+ parser = argparse.ArgumentParser(description="DevTools Advocate Agent MCP Server")
553
+ parser.add_argument(
554
+ "--log-level",
555
+ default="INFO",
556
+ choices=["DEBUG", "INFO", "WARNING", "ERROR"],
557
+ )
558
+ args = parser.parse_args()
559
+
560
+ logging.basicConfig(
561
+ level=getattr(logging, args.log_level),
562
+ format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
563
+ stream=sys.stderr, # Keep logs on stderr, JSON-RPC on stdout
564
+ )
565
+
566
+ server = MCPServer(
567
+ posthog_api_key=os.environ.get("POSTHOG_API_KEY", ""),
568
+ posthog_project_id=os.environ.get("POSTHOG_PROJECT_ID", ""),
569
+ github_token=os.environ.get("GITHUB_TOKEN", ""),
570
+ firecrawl_api_key=os.environ.get("FIRECRAWL_API_KEY", ""),
571
+ brave_api_key=os.environ.get("BRAVE_API_KEY", ""),
572
+ )
573
+
574
+ asyncio.run(server.run())
575
+
576
+
577
+ if __name__ == "__main__":
578
+ main()