opencode-bridge 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """OpenCode Bridge - MCP server for continuous OpenCode sessions."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+ """Install/uninstall opencode-bridge MCP server with Claude Code."""
3
+
4
+ import subprocess
5
+ import sys
6
+
7
+
8
+ def install():
9
+ """Register opencode-bridge as an MCP server with Claude Code."""
10
+ try:
11
+ result = subprocess.run(
12
+ ["claude", "mcp", "add", "--transport", "stdio", "--scope", "user",
13
+ "opencode-bridge", "--", "opencode-bridge"],
14
+ capture_output=True,
15
+ text=True
16
+ )
17
+ if result.returncode == 0:
18
+ print("opencode-bridge registered with Claude Code")
19
+ print(result.stdout)
20
+ else:
21
+ if "already exists" in result.stderr.lower():
22
+ print("opencode-bridge already registered")
23
+ else:
24
+ print(f"Failed to register: {result.stderr}")
25
+ sys.exit(1)
26
+ except FileNotFoundError:
27
+ print("Claude Code CLI not found. Install from: https://claude.ai/download")
28
+ sys.exit(1)
29
+
30
+
31
+ def uninstall():
32
+ """Remove opencode-bridge MCP server from Claude Code."""
33
+ try:
34
+ result = subprocess.run(
35
+ ["claude", "mcp", "remove", "opencode-bridge"],
36
+ capture_output=True,
37
+ text=True
38
+ )
39
+ if result.returncode == 0:
40
+ print("opencode-bridge removed from Claude Code")
41
+ print(result.stdout)
42
+ else:
43
+ if "not found" in result.stderr.lower():
44
+ print("opencode-bridge not registered")
45
+ else:
46
+ print(f"Failed to remove: {result.stderr}")
47
+ sys.exit(1)
48
+ except FileNotFoundError:
49
+ print("Claude Code CLI not found")
50
+ sys.exit(1)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ if len(sys.argv) > 1 and sys.argv[1] == "uninstall":
55
+ uninstall()
56
+ else:
57
+ install()
@@ -0,0 +1,817 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ OpenCode Bridge - MCP server for continuous OpenCode sessions.
4
+
5
+ Features:
6
+ - Continuous discussion sessions with conversation history
7
+ - Access to all OpenCode models (GPT-5, Claude, Gemini, etc.)
8
+ - Agent support (plan, build, explore, general)
9
+ - Session continuation
10
+ - File attachment for code review
11
+
12
+ Configuration:
13
+ - OPENCODE_MODEL: Default model (e.g., openai/gpt-5.2-codex)
14
+ - OPENCODE_AGENT: Default agent (plan, build, explore, general)
15
+ - ~/.opencode-bridge/config.json: Persistent config
16
+ """
17
+
18
+ import os
19
+ import json
20
+ import asyncio
21
+ import shutil
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+ from typing import Optional
25
+ from dataclasses import dataclass, field, asdict
26
+
27
+ from mcp.server import Server, InitializationOptions
28
+ from mcp.server.stdio import stdio_server
29
+ from mcp.types import Tool, TextContent, ServerCapabilities, ToolsCapability
30
+
31
+
32
+ # Default configuration
33
+ DEFAULT_MODEL = "openai/gpt-5.2-codex"
34
+ DEFAULT_AGENT = "plan"
35
+ DEFAULT_VARIANT = "medium"
36
+
37
+
38
+ @dataclass
39
+ class Config:
40
+ model: str = DEFAULT_MODEL
41
+ agent: str = DEFAULT_AGENT
42
+ variant: str = DEFAULT_VARIANT
43
+
44
+ @classmethod
45
+ def load(cls) -> "Config":
46
+ config = cls()
47
+
48
+ # Load from config file
49
+ config_path = Path.home() / ".opencode-bridge" / "config.json"
50
+ if config_path.exists():
51
+ try:
52
+ data = json.loads(config_path.read_text())
53
+ config.model = data.get("model", config.model)
54
+ config.agent = data.get("agent", config.agent)
55
+ config.variant = data.get("variant", config.variant)
56
+ except Exception:
57
+ pass
58
+
59
+ # Environment variables override config file
60
+ config.model = os.environ.get("OPENCODE_MODEL", config.model)
61
+ config.agent = os.environ.get("OPENCODE_AGENT", config.agent)
62
+ config.variant = os.environ.get("OPENCODE_VARIANT") or config.variant
63
+
64
+ return config
65
+
66
+ def save(self):
67
+ config_dir = Path.home() / ".opencode-bridge"
68
+ config_dir.mkdir(parents=True, exist_ok=True)
69
+ config_path = config_dir / "config.json"
70
+ data = {"model": self.model, "agent": self.agent, "variant": self.variant}
71
+ config_path.write_text(json.dumps(data, indent=2))
72
+
73
+
74
+ def find_opencode() -> Optional[Path]:
75
+ """Find opencode binary."""
76
+ # Check common locations
77
+ paths = [
78
+ Path.home() / ".opencode" / "bin" / "opencode",
79
+ Path("/usr/local/bin/opencode"),
80
+ Path("/usr/bin/opencode"),
81
+ ]
82
+ for p in paths:
83
+ if p.exists():
84
+ return p
85
+ # Check PATH
86
+ which = shutil.which("opencode")
87
+ if which:
88
+ return Path(which)
89
+ return None
90
+
91
+
92
+ OPENCODE_BIN = find_opencode()
93
+
94
+
95
+ @dataclass
96
+ class Message:
97
+ role: str
98
+ content: str
99
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
100
+
101
+
102
+ @dataclass
103
+ class Session:
104
+ id: str
105
+ model: str
106
+ agent: str
107
+ variant: str = DEFAULT_VARIANT
108
+ opencode_session_id: Optional[str] = None
109
+ messages: list[Message] = field(default_factory=list)
110
+ created: str = field(default_factory=lambda: datetime.now().isoformat())
111
+
112
+ def add_message(self, role: str, content: str):
113
+ self.messages.append(Message(role=role, content=content))
114
+
115
+ def save(self, path: Path):
116
+ data = {
117
+ "id": self.id,
118
+ "model": self.model,
119
+ "agent": self.agent,
120
+ "variant": self.variant,
121
+ "opencode_session_id": self.opencode_session_id,
122
+ "created": self.created,
123
+ "messages": [asdict(m) for m in self.messages]
124
+ }
125
+ path.write_text(json.dumps(data, indent=2))
126
+
127
+ @classmethod
128
+ def load(cls, path: Path) -> "Session":
129
+ data = json.loads(path.read_text())
130
+ session = cls(
131
+ id=data["id"],
132
+ model=data["model"],
133
+ agent=data.get("agent", DEFAULT_AGENT),
134
+ variant=data.get("variant", DEFAULT_VARIANT),
135
+ opencode_session_id=data.get("opencode_session_id"),
136
+ created=data.get("created", datetime.now().isoformat())
137
+ )
138
+ for m in data.get("messages", []):
139
+ session.messages.append(Message(**m))
140
+ return session
141
+
142
+
143
+ class OpenCodeBridge:
144
+ def __init__(self):
145
+ self.start_time = datetime.now()
146
+ self.config = Config.load()
147
+ self.sessions: dict[str, Session] = {}
148
+ self.active_session: Optional[str] = None
149
+ self.sessions_dir = Path.home() / ".opencode-bridge" / "sessions"
150
+ self.sessions_dir.mkdir(parents=True, exist_ok=True)
151
+ self.available_models: list[str] = []
152
+ self.available_agents: list[str] = []
153
+ self._load_sessions()
154
+
155
+ def _load_sessions(self):
156
+ for path in self.sessions_dir.glob("*.json"):
157
+ try:
158
+ session = Session.load(path)
159
+ self.sessions[session.id] = session
160
+ except Exception:
161
+ pass
162
+
163
+ async def _run_opencode(self, *args, timeout: int = 300) -> tuple[str, int]:
164
+ """Run opencode CLI command and return output (async)."""
165
+ if not OPENCODE_BIN:
166
+ return "OpenCode not installed. Install from: https://opencode.ai", 1
167
+
168
+ try:
169
+ proc = await asyncio.create_subprocess_exec(
170
+ str(OPENCODE_BIN), *args,
171
+ stdin=asyncio.subprocess.PIPE,
172
+ stdout=asyncio.subprocess.PIPE,
173
+ stderr=asyncio.subprocess.PIPE
174
+ )
175
+ stdout, stderr = await asyncio.wait_for(
176
+ proc.communicate(input=b''),
177
+ timeout=timeout
178
+ )
179
+ output = stdout.decode() or stderr.decode()
180
+ return output.strip(), proc.returncode or 0
181
+ except asyncio.TimeoutError:
182
+ proc.kill()
183
+ await proc.wait()
184
+ return "Command timed out", 1
185
+ except Exception as e:
186
+ return f"Error: {e}", 1
187
+
188
+ async def list_models(self, provider: Optional[str] = None) -> str:
189
+ """List available models from OpenCode."""
190
+ args = ["models"]
191
+ if provider:
192
+ args.append(provider)
193
+
194
+ output, code = await self._run_opencode(*args)
195
+ if code != 0:
196
+ return f"Error listing models: {output}"
197
+
198
+ self.available_models = [line.strip() for line in output.split("\n") if line.strip()]
199
+
200
+ # Group by provider
201
+ providers: dict[str, list[str]] = {}
202
+ for model in self.available_models:
203
+ if "/" in model:
204
+ prov, name = model.split("/", 1)
205
+ else:
206
+ prov, name = "other", model
207
+ providers.setdefault(prov, []).append(name)
208
+
209
+ lines = ["Available models:"]
210
+ for prov in sorted(providers.keys()):
211
+ lines.append(f"\n**{prov}:**")
212
+ for name in sorted(providers[prov]):
213
+ full = f"{prov}/{name}"
214
+ lines.append(f" - {full}")
215
+
216
+ return "\n".join(lines)
217
+
218
+ async def list_agents(self) -> str:
219
+ """List available agents from OpenCode."""
220
+ output, code = await self._run_opencode("agent", "list")
221
+ if code != 0:
222
+ return f"Error listing agents: {output}"
223
+
224
+ # Parse agent names from output
225
+ agents = []
226
+ for line in output.split("\n"):
227
+ line = line.strip()
228
+ if line and "(" in line:
229
+ name = line.split("(")[0].strip()
230
+ agents.append(name)
231
+
232
+ self.available_agents = agents
233
+ return "Available agents:\n" + "\n".join(f" - {a}" for a in agents)
234
+
235
+ async def start_session(
236
+ self,
237
+ session_id: str,
238
+ model: Optional[str] = None,
239
+ agent: Optional[str] = None,
240
+ variant: Optional[str] = None
241
+ ) -> str:
242
+ # Use config defaults if not specified
243
+ model = model or self.config.model
244
+ agent = agent or self.config.agent
245
+ variant = variant or self.config.variant
246
+
247
+ session = Session(
248
+ id=session_id,
249
+ model=model,
250
+ agent=agent,
251
+ variant=variant
252
+ )
253
+ self.sessions[session_id] = session
254
+ self.active_session = session_id
255
+ session.save(self.sessions_dir / f"{session_id}.json")
256
+
257
+ result = f"Session '{session_id}' started\n Model: {model}\n Agent: {agent}"
258
+ if variant:
259
+ result += f"\n Variant: {variant}"
260
+ return result
261
+
262
+ def get_config(self) -> str:
263
+ """Get current configuration."""
264
+ return f"""Current configuration:
265
+ Model: {self.config.model}
266
+ Agent: {self.config.agent}
267
+ Variant: {self.config.variant}
268
+
269
+ Set via:
270
+ - ~/.opencode-bridge/config.json
271
+ - OPENCODE_MODEL, OPENCODE_AGENT, OPENCODE_VARIANT env vars
272
+ - opencode_configure tool"""
273
+
274
+ def set_config(self, model: Optional[str] = None, agent: Optional[str] = None, variant: Optional[str] = None) -> str:
275
+ """Update and persist configuration."""
276
+ changes = []
277
+ if model:
278
+ self.config.model = model
279
+ changes.append(f"model: {model}")
280
+ if agent:
281
+ self.config.agent = agent
282
+ changes.append(f"agent: {agent}")
283
+ if variant:
284
+ self.config.variant = variant
285
+ changes.append(f"variant: {variant}")
286
+
287
+ if changes:
288
+ self.config.save()
289
+ return "Configuration updated:\n " + "\n ".join(changes)
290
+ return "No changes made."
291
+
292
+ async def send_message(
293
+ self,
294
+ message: str,
295
+ session_id: Optional[str] = None,
296
+ files: Optional[list[str]] = None
297
+ ) -> str:
298
+ sid = session_id or self.active_session
299
+ if not sid or sid not in self.sessions:
300
+ return "No active session. Use opencode_start first."
301
+
302
+ session = self.sessions[sid]
303
+ session.add_message("user", message)
304
+
305
+ # Build command - message must come right after "run" as positional arg
306
+ args = ["run", message]
307
+ args.extend(["--model", session.model])
308
+ args.extend(["--agent", session.agent])
309
+
310
+ # Add variant if specified
311
+ if session.variant:
312
+ args.extend(["--variant", session.variant])
313
+
314
+ # Continue session if we have an opencode session ID
315
+ if session.opencode_session_id:
316
+ args.extend(["--session", session.opencode_session_id])
317
+
318
+ # Attach files
319
+ if files:
320
+ for f in files:
321
+ args.extend(["--file", f])
322
+
323
+ # Use JSON format to get session ID
324
+ args.extend(["--format", "json"])
325
+
326
+ output, code = await self._run_opencode(*args)
327
+
328
+ if code != 0:
329
+ return f"Error: {output}"
330
+
331
+ # Parse JSON events for session ID and text
332
+ reply_parts = []
333
+ for line in output.split("\n"):
334
+ if not line:
335
+ continue
336
+ try:
337
+ event = json.loads(line)
338
+ if not session.opencode_session_id and "sessionID" in event:
339
+ session.opencode_session_id = event["sessionID"]
340
+ if event.get("type") == "text":
341
+ text = event.get("part", {}).get("text", "")
342
+ if text:
343
+ reply_parts.append(text)
344
+ except json.JSONDecodeError:
345
+ continue
346
+
347
+ reply = "".join(reply_parts)
348
+ if reply:
349
+ session.add_message("assistant", reply)
350
+
351
+ # Save if we got a reply or captured a new session ID
352
+ if reply or session.opencode_session_id:
353
+ session.save(self.sessions_dir / f"{sid}.json")
354
+
355
+ return reply or "No response received"
356
+
357
+ async def plan(
358
+ self,
359
+ task: str,
360
+ session_id: Optional[str] = None,
361
+ files: Optional[list[str]] = None
362
+ ) -> str:
363
+ """Start a planning discussion using the plan agent."""
364
+ sid = session_id or self.active_session
365
+
366
+ # If no active session, create one for planning
367
+ if not sid or sid not in self.sessions:
368
+ sid = f"plan-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
369
+ await self.start_session(sid, agent="plan")
370
+
371
+ # Switch to plan agent if not already
372
+ session = self.sessions[sid]
373
+ if session.agent != "plan":
374
+ session.agent = "plan"
375
+ session.save(self.sessions_dir / f"{sid}.json")
376
+
377
+ return await self.send_message(task, sid, files)
378
+
379
+ async def brainstorm(
380
+ self,
381
+ topic: str,
382
+ session_id: Optional[str] = None
383
+ ) -> str:
384
+ """Open-ended brainstorming discussion."""
385
+ sid = session_id or self.active_session
386
+
387
+ if not sid or sid not in self.sessions:
388
+ sid = f"brainstorm-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
389
+ await self.start_session(sid, agent="build")
390
+
391
+ prompt = f"""Let's brainstorm about: {topic}
392
+
393
+ Please provide:
394
+ 1. Key considerations and trade-offs
395
+ 2. Multiple approaches or solutions
396
+ 3. Pros and cons of each approach
397
+ 4. Your recommended approach and why"""
398
+
399
+ return await self.send_message(prompt, sid)
400
+
401
+ async def review_code(
402
+ self,
403
+ code_or_file: str,
404
+ focus: str = "correctness, efficiency, and potential bugs",
405
+ session_id: Optional[str] = None
406
+ ) -> str:
407
+ """Review code for issues and improvements."""
408
+ sid = session_id or self.active_session
409
+
410
+ if not sid or sid not in self.sessions:
411
+ sid = f"review-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
412
+ await self.start_session(sid, agent="build")
413
+
414
+ # Check if it's a file path
415
+ files = None
416
+ if Path(code_or_file).is_file():
417
+ files = [code_or_file]
418
+ prompt = f"Please review the attached code file, focusing on: {focus}"
419
+ else:
420
+ prompt = f"""Please review this code, focusing on: {focus}
421
+
422
+ ```
423
+ {code_or_file}
424
+ ```"""
425
+
426
+ return await self.send_message(prompt, sid, files)
427
+
428
+ def list_sessions(self) -> str:
429
+ if not self.sessions:
430
+ return "No sessions found."
431
+
432
+ lines = ["Sessions:"]
433
+ for sid, session in self.sessions.items():
434
+ active = " (active)" if sid == self.active_session else ""
435
+ msg_count = len(session.messages)
436
+ variant_str = f", variant={session.variant}" if session.variant else ""
437
+ lines.append(f" - {sid}: {session.model} [{session.agent}{variant_str}], {msg_count} messages{active}")
438
+ return "\n".join(lines)
439
+
440
+ def get_history(self, session_id: Optional[str] = None, last_n: int = 20) -> str:
441
+ sid = session_id or self.active_session
442
+ if not sid or sid not in self.sessions:
443
+ return "No active session."
444
+
445
+ session = self.sessions[sid]
446
+ variant_str = f", Variant: {session.variant}" if session.variant else ""
447
+ lines = [f"Session: {sid}", f"Model: {session.model}, Agent: {session.agent}{variant_str}", "---"]
448
+
449
+ for msg in session.messages[-last_n:]:
450
+ role = "You" if msg.role == "user" else "OpenCode"
451
+ lines.append(f"\n**{role}:**\n{msg.content}")
452
+
453
+ return "\n".join(lines)
454
+
455
+ def set_active(self, session_id: str) -> str:
456
+ if session_id not in self.sessions:
457
+ return f"Session '{session_id}' not found."
458
+ self.active_session = session_id
459
+ session = self.sessions[session_id]
460
+ variant_str = f", variant={session.variant}" if session.variant else ""
461
+ return f"Active session: '{session_id}' ({session.model}, {session.agent}{variant_str})"
462
+
463
+ def set_model(self, model: str, session_id: Optional[str] = None) -> str:
464
+ sid = session_id or self.active_session
465
+ if not sid or sid not in self.sessions:
466
+ return "No active session."
467
+
468
+ session = self.sessions[sid]
469
+ old_model = session.model
470
+ session.model = model
471
+ session.save(self.sessions_dir / f"{sid}.json")
472
+
473
+ return f"Model changed: {old_model} -> {model}"
474
+
475
+ def set_agent(self, agent: str, session_id: Optional[str] = None) -> str:
476
+ sid = session_id or self.active_session
477
+ if not sid or sid not in self.sessions:
478
+ return "No active session."
479
+
480
+ session = self.sessions[sid]
481
+ old_agent = session.agent
482
+ session.agent = agent
483
+ session.save(self.sessions_dir / f"{sid}.json")
484
+
485
+ return f"Agent changed: {old_agent} -> {agent}"
486
+
487
+ def set_variant(self, variant: Optional[str], session_id: Optional[str] = None) -> str:
488
+ sid = session_id or self.active_session
489
+ if not sid or sid not in self.sessions:
490
+ return "No active session."
491
+
492
+ session = self.sessions[sid]
493
+ old_variant = session.variant or "none"
494
+ session.variant = variant
495
+ session.save(self.sessions_dir / f"{sid}.json")
496
+
497
+ new_variant = variant or "none"
498
+ return f"Variant changed: {old_variant} -> {new_variant}"
499
+
500
+ def end_session(self, session_id: Optional[str] = None) -> str:
501
+ sid = session_id or self.active_session
502
+ if not sid or sid not in self.sessions:
503
+ return "No active session to end."
504
+
505
+ del self.sessions[sid]
506
+ session_path = self.sessions_dir / f"{sid}.json"
507
+ if session_path.exists():
508
+ session_path.unlink()
509
+
510
+ if self.active_session == sid:
511
+ self.active_session = None
512
+
513
+ return f"Session '{sid}' ended."
514
+
515
+ def health_check(self) -> dict:
516
+ """Return server health status."""
517
+ uptime_seconds = int((datetime.now() - self.start_time).total_seconds())
518
+ return {
519
+ "status": "ok",
520
+ "sessions": len(self.sessions),
521
+ "uptime": uptime_seconds
522
+ }
523
+
524
+
525
+ # MCP Server setup
526
+ bridge = OpenCodeBridge()
527
+ server = Server("opencode-bridge")
528
+
529
+
530
+ @server.list_tools()
531
+ async def list_tools():
532
+ return [
533
+ Tool(
534
+ name="opencode_models",
535
+ description="List available models from OpenCode (GPT-5, Claude, Gemini, etc.)",
536
+ inputSchema={
537
+ "type": "object",
538
+ "properties": {
539
+ "provider": {
540
+ "type": "string",
541
+ "description": "Filter by provider (openai, github-copilot, anthropic)"
542
+ }
543
+ }
544
+ }
545
+ ),
546
+ Tool(
547
+ name="opencode_agents",
548
+ description="List available agents (plan, build, explore, general)",
549
+ inputSchema={"type": "object", "properties": {}}
550
+ ),
551
+ Tool(
552
+ name="opencode_start",
553
+ description="Start a new discussion session with OpenCode",
554
+ inputSchema={
555
+ "type": "object",
556
+ "properties": {
557
+ "session_id": {
558
+ "type": "string",
559
+ "description": "Unique identifier for this session"
560
+ },
561
+ "model": {
562
+ "type": "string",
563
+ "description": "Model to use (default: openai/gpt-5.2-codex)"
564
+ },
565
+ "agent": {
566
+ "type": "string",
567
+ "description": "Agent to use: plan, build, explore, general (default: plan)"
568
+ },
569
+ "variant": {
570
+ "type": "string",
571
+ "description": "Model variant for reasoning effort: minimal, low, medium, high, xhigh, max (default: medium)"
572
+ }
573
+ },
574
+ "required": ["session_id"]
575
+ }
576
+ ),
577
+ Tool(
578
+ name="opencode_discuss",
579
+ description="Send a message to OpenCode. Use for code review, architecture, brainstorming.",
580
+ inputSchema={
581
+ "type": "object",
582
+ "properties": {
583
+ "message": {
584
+ "type": "string",
585
+ "description": "Your message or question"
586
+ },
587
+ "files": {
588
+ "type": "array",
589
+ "items": {"type": "string"},
590
+ "description": "File paths to attach for context"
591
+ }
592
+ },
593
+ "required": ["message"]
594
+ }
595
+ ),
596
+ Tool(
597
+ name="opencode_plan",
598
+ description="Start a planning discussion with the plan agent",
599
+ inputSchema={
600
+ "type": "object",
601
+ "properties": {
602
+ "task": {
603
+ "type": "string",
604
+ "description": "What to plan"
605
+ },
606
+ "files": {
607
+ "type": "array",
608
+ "items": {"type": "string"},
609
+ "description": "Relevant file paths"
610
+ }
611
+ },
612
+ "required": ["task"]
613
+ }
614
+ ),
615
+ Tool(
616
+ name="opencode_brainstorm",
617
+ description="Open-ended brainstorming on a topic",
618
+ inputSchema={
619
+ "type": "object",
620
+ "properties": {
621
+ "topic": {
622
+ "type": "string",
623
+ "description": "Topic to brainstorm about"
624
+ }
625
+ },
626
+ "required": ["topic"]
627
+ }
628
+ ),
629
+ Tool(
630
+ name="opencode_review",
631
+ description="Review code for issues and improvements",
632
+ inputSchema={
633
+ "type": "object",
634
+ "properties": {
635
+ "code_or_file": {
636
+ "type": "string",
637
+ "description": "Code snippet or file path"
638
+ },
639
+ "focus": {
640
+ "type": "string",
641
+ "description": "What to focus on (default: correctness, efficiency, bugs)"
642
+ }
643
+ },
644
+ "required": ["code_or_file"]
645
+ }
646
+ ),
647
+ Tool(
648
+ name="opencode_model",
649
+ description="Change the model for the current session",
650
+ inputSchema={
651
+ "type": "object",
652
+ "properties": {
653
+ "model": {"type": "string", "description": "New model"}
654
+ },
655
+ "required": ["model"]
656
+ }
657
+ ),
658
+ Tool(
659
+ name="opencode_agent",
660
+ description="Change the agent for the current session",
661
+ inputSchema={
662
+ "type": "object",
663
+ "properties": {
664
+ "agent": {"type": "string", "description": "New agent (plan, build, explore, general)"}
665
+ },
666
+ "required": ["agent"]
667
+ }
668
+ ),
669
+ Tool(
670
+ name="opencode_variant",
671
+ description="Change the model variant (reasoning effort) for the current session",
672
+ inputSchema={
673
+ "type": "object",
674
+ "properties": {
675
+ "variant": {"type": "string", "description": "New variant: minimal, low, medium, high, xhigh, max"}
676
+ },
677
+ "required": ["variant"]
678
+ }
679
+ ),
680
+ Tool(
681
+ name="opencode_history",
682
+ description="Get conversation history",
683
+ inputSchema={
684
+ "type": "object",
685
+ "properties": {
686
+ "last_n": {"type": "integer", "description": "Number of messages (default: 20)"}
687
+ }
688
+ }
689
+ ),
690
+ Tool(
691
+ name="opencode_sessions",
692
+ description="List all sessions",
693
+ inputSchema={"type": "object", "properties": {}}
694
+ ),
695
+ Tool(
696
+ name="opencode_switch",
697
+ description="Switch to a different session",
698
+ inputSchema={
699
+ "type": "object",
700
+ "properties": {
701
+ "session_id": {"type": "string", "description": "Session to switch to"}
702
+ },
703
+ "required": ["session_id"]
704
+ }
705
+ ),
706
+ Tool(
707
+ name="opencode_end",
708
+ description="End the current session",
709
+ inputSchema={"type": "object", "properties": {}}
710
+ ),
711
+ Tool(
712
+ name="opencode_config",
713
+ description="Get current configuration (default model, agent, variant)",
714
+ inputSchema={"type": "object", "properties": {}}
715
+ ),
716
+ Tool(
717
+ name="opencode_configure",
718
+ description="Set default model, agent, and/or variant (persisted)",
719
+ inputSchema={
720
+ "type": "object",
721
+ "properties": {
722
+ "model": {"type": "string", "description": "Default model"},
723
+ "agent": {"type": "string", "description": "Default agent"},
724
+ "variant": {"type": "string", "description": "Default variant: minimal, low, medium, high, xhigh, max"}
725
+ }
726
+ }
727
+ ),
728
+ Tool(
729
+ name="opencode_health",
730
+ description="Health check: returns server status, session count, and uptime",
731
+ inputSchema={"type": "object", "properties": {}}
732
+ )
733
+ ]
734
+
735
+
736
+ @server.call_tool()
737
+ async def call_tool(name: str, arguments: dict):
738
+ try:
739
+ if name == "opencode_models":
740
+ result = await bridge.list_models(arguments.get("provider"))
741
+ elif name == "opencode_agents":
742
+ result = await bridge.list_agents()
743
+ elif name == "opencode_start":
744
+ result = await bridge.start_session(
745
+ session_id=arguments["session_id"],
746
+ model=arguments.get("model"),
747
+ agent=arguments.get("agent"),
748
+ variant=arguments.get("variant")
749
+ )
750
+ elif name == "opencode_discuss":
751
+ result = await bridge.send_message(
752
+ message=arguments["message"],
753
+ files=arguments.get("files")
754
+ )
755
+ elif name == "opencode_plan":
756
+ result = await bridge.plan(
757
+ task=arguments["task"],
758
+ files=arguments.get("files")
759
+ )
760
+ elif name == "opencode_brainstorm":
761
+ result = await bridge.brainstorm(arguments["topic"])
762
+ elif name == "opencode_review":
763
+ result = await bridge.review_code(
764
+ code_or_file=arguments["code_or_file"],
765
+ focus=arguments.get("focus", "correctness, efficiency, and potential bugs")
766
+ )
767
+ elif name == "opencode_model":
768
+ result = bridge.set_model(arguments["model"])
769
+ elif name == "opencode_agent":
770
+ result = bridge.set_agent(arguments["agent"])
771
+ elif name == "opencode_variant":
772
+ result = bridge.set_variant(arguments["variant"])
773
+ elif name == "opencode_history":
774
+ result = bridge.get_history(last_n=arguments.get("last_n", 20))
775
+ elif name == "opencode_sessions":
776
+ result = bridge.list_sessions()
777
+ elif name == "opencode_switch":
778
+ result = bridge.set_active(arguments["session_id"])
779
+ elif name == "opencode_end":
780
+ result = bridge.end_session()
781
+ elif name == "opencode_config":
782
+ result = bridge.get_config()
783
+ elif name == "opencode_configure":
784
+ result = bridge.set_config(
785
+ model=arguments.get("model"),
786
+ agent=arguments.get("agent"),
787
+ variant=arguments.get("variant")
788
+ )
789
+ elif name == "opencode_health":
790
+ health = bridge.health_check()
791
+ result = f"Status: {health['status']}\nSessions: {health['sessions']}\nUptime: {health['uptime']}s"
792
+ else:
793
+ result = f"Unknown tool: {name}"
794
+
795
+ return [TextContent(type="text", text=result)]
796
+
797
+ except Exception as e:
798
+ return [TextContent(type="text", text=f"Error: {e}")]
799
+
800
+
801
+ def main():
802
+ import asyncio
803
+
804
+ async def run():
805
+ init_options = InitializationOptions(
806
+ server_name="opencode-bridge",
807
+ server_version="0.1.0",
808
+ capabilities=ServerCapabilities(tools=ToolsCapability())
809
+ )
810
+ async with stdio_server() as (read_stream, write_stream):
811
+ await server.run(read_stream, write_stream, init_options)
812
+
813
+ asyncio.run(run())
814
+
815
+
816
+ if __name__ == "__main__":
817
+ main()
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-bridge
3
+ Version: 0.1.3
4
+ Summary: MCP server for continuous OpenCode discussion sessions
5
+ Project-URL: Repository, https://github.com/genomewalker/opencode-bridge
6
+ Author: Antonio Fernandez-Guerra
7
+ License-Expression: MIT
8
+ Keywords: claude,code-review,discussion,gpt,mcp,opencode
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: mcp>=1.0.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # OpenCode Bridge
24
+
25
+ MCP server for continuous discussion sessions with OpenCode. Collaborate with GPT-5, Claude, Gemini, and other models through Claude Code.
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ # 1. Install
31
+ uv pip install git+https://github.com/genomewalker/opencode-bridge.git
32
+
33
+ # 2. Register with Claude Code
34
+ opencode-bridge-install
35
+
36
+ # 3. Use in Claude Code
37
+ # The tools are now available - Claude will use them automatically
38
+ ```
39
+
40
+ ## Features
41
+
42
+ - **Continuous sessions**: Conversation history persists across messages
43
+ - **Multiple models**: Access all OpenCode models (GPT-5.x, Claude, Gemini, etc.)
44
+ - **Agent support**: plan, build, explore, general agents
45
+ - **Variant control**: Set reasoning effort (minimal → max)
46
+ - **File attachment**: Share code files for review
47
+ - **Session continuity**: Conversations continue across tool calls
48
+
49
+ ## Installation
50
+
51
+ ### With uv (recommended)
52
+
53
+ ```bash
54
+ uv pip install git+https://github.com/genomewalker/opencode-bridge.git
55
+ ```
56
+
57
+ ### With pip
58
+
59
+ ```bash
60
+ pip install git+https://github.com/genomewalker/opencode-bridge.git
61
+ ```
62
+
63
+ ### From source
64
+
65
+ ```bash
66
+ git clone https://github.com/genomewalker/opencode-bridge.git
67
+ cd opencode-bridge
68
+ pip install -e .
69
+ ```
70
+
71
+ ## Register with Claude Code
72
+
73
+ ```bash
74
+ # Install (registers MCP server)
75
+ opencode-bridge-install
76
+
77
+ # Verify
78
+ claude mcp list
79
+
80
+ # Uninstall
81
+ opencode-bridge-uninstall
82
+ ```
83
+
84
+ ## Available Models
85
+
86
+ | Provider | Models |
87
+ |----------|--------|
88
+ | openai | gpt-5.2-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini |
89
+ | github-copilot | claude-opus-4.5, claude-sonnet-4.5, gpt-5, gemini-2.5-pro |
90
+ | opencode | gpt-5-nano (free), glm-4.7-free, grok-code |
91
+
92
+ Run `opencode models` to see all available models.
93
+
94
+ ## MCP Tools
95
+
96
+ | Tool | Description |
97
+ |------|-------------|
98
+ | `opencode_start` | Start a new session |
99
+ | `opencode_discuss` | Send a message |
100
+ | `opencode_plan` | Start planning discussion |
101
+ | `opencode_brainstorm` | Open-ended brainstorming |
102
+ | `opencode_review` | Review code |
103
+ | `opencode_models` | List available models |
104
+ | `opencode_agents` | List available agents |
105
+ | `opencode_model` | Change session model |
106
+ | `opencode_agent` | Change session agent |
107
+ | `opencode_variant` | Change reasoning effort |
108
+ | `opencode_config` | Show current configuration |
109
+ | `opencode_configure` | Set defaults (persisted) |
110
+ | `opencode_history` | Show conversation history |
111
+ | `opencode_sessions` | List all sessions |
112
+ | `opencode_switch` | Switch to another session |
113
+ | `opencode_end` | End current session |
114
+ | `opencode_health` | Server health check |
115
+
116
+ ## Configuration
117
+
118
+ ### Environment variables
119
+
120
+ ```bash
121
+ export OPENCODE_MODEL="openai/gpt-5.2-codex"
122
+ export OPENCODE_AGENT="plan"
123
+ export OPENCODE_VARIANT="medium"
124
+ ```
125
+
126
+ ### Config file
127
+
128
+ `~/.opencode-bridge/config.json`:
129
+ ```json
130
+ {
131
+ "model": "openai/gpt-5.2-codex",
132
+ "agent": "plan",
133
+ "variant": "medium"
134
+ }
135
+ ```
136
+
137
+ ### Variants (reasoning effort)
138
+
139
+ `minimal` → `low` → `medium` → `high` → `xhigh` → `max`
140
+
141
+ Higher variants use more reasoning tokens for complex tasks.
142
+
143
+ ## Requirements
144
+
145
+ - Python 3.10+
146
+ - [OpenCode CLI](https://opencode.ai) installed
147
+ - Claude Code
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,7 @@
1
+ opencode_bridge/__init__.py,sha256=SkXVg907MuInd7UEYOjHjiiIIT46y4S2l20hE9cShKo,92
2
+ opencode_bridge/install.py,sha256=VOJNYUPxq88g0XizkHSQ9noM3Qcd3AfZxPUZInEKErk,1796
3
+ opencode_bridge/server.py,sha256=iQ2SHPy9rRp8K3hpRxOGgXcw7VdT9JX0amjUS87A6Fk,28475
4
+ opencode_bridge-0.1.3.dist-info/METADATA,sha256=Y6YMgJvGUT2k-Jr-wz1Npr_VUugcgkKy6gceHr8ND_0,3924
5
+ opencode_bridge-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ opencode_bridge-0.1.3.dist-info/entry_points.txt,sha256=8elAgeI-Sk7EPoV7kUr3CCgQyIAW2VfDj5ZXQ_9slCc,184
7
+ opencode_bridge-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ opencode-bridge = opencode_bridge.server:main
3
+ opencode-bridge-install = opencode_bridge.install:install
4
+ opencode-bridge-uninstall = opencode_bridge.install:uninstall