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.
- devrel_origin/__init__.py +15 -0
- devrel_origin/cli/__init__.py +92 -0
- devrel_origin/cli/_common.py +243 -0
- devrel_origin/cli/analytics.py +28 -0
- devrel_origin/cli/argus.py +497 -0
- devrel_origin/cli/auth.py +227 -0
- devrel_origin/cli/config.py +108 -0
- devrel_origin/cli/content.py +259 -0
- devrel_origin/cli/cost.py +108 -0
- devrel_origin/cli/cro.py +298 -0
- devrel_origin/cli/deliverables.py +65 -0
- devrel_origin/cli/docs.py +91 -0
- devrel_origin/cli/doctor.py +178 -0
- devrel_origin/cli/experiment.py +29 -0
- devrel_origin/cli/growth.py +97 -0
- devrel_origin/cli/init.py +472 -0
- devrel_origin/cli/intel.py +27 -0
- devrel_origin/cli/kb.py +96 -0
- devrel_origin/cli/listen.py +31 -0
- devrel_origin/cli/marketing.py +66 -0
- devrel_origin/cli/migrate.py +45 -0
- devrel_origin/cli/run.py +46 -0
- devrel_origin/cli/sales.py +57 -0
- devrel_origin/cli/schedule.py +62 -0
- devrel_origin/cli/synthesize.py +28 -0
- devrel_origin/cli/triage.py +29 -0
- devrel_origin/cli/video.py +35 -0
- devrel_origin/core/__init__.py +58 -0
- devrel_origin/core/agent_config.py +75 -0
- devrel_origin/core/argus.py +964 -0
- devrel_origin/core/atlas.py +1450 -0
- devrel_origin/core/base.py +372 -0
- devrel_origin/core/cyra.py +563 -0
- devrel_origin/core/dex.py +708 -0
- devrel_origin/core/echo.py +614 -0
- devrel_origin/core/growth/__init__.py +27 -0
- devrel_origin/core/growth/recommendations.py +219 -0
- devrel_origin/core/growth/target_kinds.py +51 -0
- devrel_origin/core/iris.py +513 -0
- devrel_origin/core/kai.py +1367 -0
- devrel_origin/core/llm.py +542 -0
- devrel_origin/core/llm_backends.py +274 -0
- devrel_origin/core/mox.py +514 -0
- devrel_origin/core/nova.py +349 -0
- devrel_origin/core/pax.py +1205 -0
- devrel_origin/core/rex.py +532 -0
- devrel_origin/core/sage.py +486 -0
- devrel_origin/core/sentinel.py +385 -0
- devrel_origin/core/types.py +98 -0
- devrel_origin/core/video/__init__.py +22 -0
- devrel_origin/core/video/assembler.py +131 -0
- devrel_origin/core/video/browser_recorder.py +118 -0
- devrel_origin/core/video/desktop_recorder.py +254 -0
- devrel_origin/core/video/overlay_renderer.py +143 -0
- devrel_origin/core/video/script_parser.py +147 -0
- devrel_origin/core/video/tts_engine.py +82 -0
- devrel_origin/core/vox.py +268 -0
- devrel_origin/core/watchdog.py +321 -0
- devrel_origin/project/__init__.py +1 -0
- devrel_origin/project/config.py +75 -0
- devrel_origin/project/cost_sink.py +61 -0
- devrel_origin/project/init.py +104 -0
- devrel_origin/project/paths.py +75 -0
- devrel_origin/project/state.py +241 -0
- devrel_origin/project/templates/__init__.py +4 -0
- devrel_origin/project/templates/config.toml +24 -0
- devrel_origin/project/templates/devrel.gitignore +10 -0
- devrel_origin/project/templates/slop-blocklist.md +45 -0
- devrel_origin/project/templates/style.md +24 -0
- devrel_origin/project/templates/voice.md +29 -0
- devrel_origin/quality/__init__.py +66 -0
- devrel_origin/quality/editorial.py +357 -0
- devrel_origin/quality/persona.py +84 -0
- devrel_origin/quality/readability.py +148 -0
- devrel_origin/quality/slop.py +167 -0
- devrel_origin/quality/style.py +110 -0
- devrel_origin/quality/voice.py +15 -0
- devrel_origin/tools/__init__.py +9 -0
- devrel_origin/tools/analytics.py +304 -0
- devrel_origin/tools/api_client.py +393 -0
- devrel_origin/tools/apollo_client.py +305 -0
- devrel_origin/tools/code_validator.py +428 -0
- devrel_origin/tools/github_tools.py +297 -0
- devrel_origin/tools/instantly_client.py +412 -0
- devrel_origin/tools/kb_harvester.py +340 -0
- devrel_origin/tools/mcp_server.py +578 -0
- devrel_origin/tools/notifications.py +245 -0
- devrel_origin/tools/run_report.py +193 -0
- devrel_origin/tools/scheduler.py +231 -0
- devrel_origin/tools/search_tools.py +321 -0
- devrel_origin/tools/self_improve.py +168 -0
- devrel_origin/tools/sheets.py +236 -0
- devrel_origin-0.2.14.dist-info/METADATA +354 -0
- devrel_origin-0.2.14.dist-info/RECORD +98 -0
- devrel_origin-0.2.14.dist-info/WHEEL +5 -0
- devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
- devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
- 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()
|