letswork 2.0.0__tar.gz → 2.0.2__tar.gz

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 (34) hide show
  1. {letswork-2.0.0 → letswork-2.0.2}/PKG-INFO +2 -3
  2. {letswork-2.0.0 → letswork-2.0.2}/letswork/cli.py +49 -35
  3. {letswork-2.0.0 → letswork-2.0.2}/letswork/launcher.py +5 -19
  4. letswork-2.0.2/letswork/proxy.py +130 -0
  5. {letswork-2.0.0 → letswork-2.0.2}/letswork/remote_client.py +0 -9
  6. {letswork-2.0.0 → letswork-2.0.2}/letswork/server.py +8 -37
  7. {letswork-2.0.0 → letswork-2.0.2}/pyproject.toml +2 -4
  8. letswork-2.0.0/letswork/proxy.py +0 -109
  9. {letswork-2.0.0 → letswork-2.0.2}/.github/workflows/ci.yml +0 -0
  10. {letswork-2.0.0 → letswork-2.0.2}/.github/workflows/publish.yml +0 -0
  11. {letswork-2.0.0 → letswork-2.0.2}/.gitignore +0 -0
  12. {letswork-2.0.0 → letswork-2.0.2}/README.md +0 -0
  13. {letswork-2.0.0 → letswork-2.0.2}/docs/architecture.md +0 -0
  14. {letswork-2.0.0 → letswork-2.0.2}/docs/spec.md +0 -0
  15. {letswork-2.0.0 → letswork-2.0.2}/docs/tasks.md +0 -0
  16. {letswork-2.0.0 → letswork-2.0.2}/letswork/__init__.py +0 -0
  17. {letswork-2.0.0 → letswork-2.0.2}/letswork/approval.py +0 -0
  18. {letswork-2.0.0 → letswork-2.0.2}/letswork/auth.py +0 -0
  19. {letswork-2.0.0 → letswork-2.0.2}/letswork/events.py +0 -0
  20. {letswork-2.0.0 → letswork-2.0.2}/letswork/filelock.py +0 -0
  21. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/__init__.py +0 -0
  22. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/app.py +0 -0
  23. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/approval_panel.py +0 -0
  24. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/chat.py +0 -0
  25. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/chat_app.py +0 -0
  26. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/file_tree.py +0 -0
  27. {letswork-2.0.0 → letswork-2.0.2}/letswork/tui/file_viewer.py +0 -0
  28. {letswork-2.0.0 → letswork-2.0.2}/letswork/tunnel.py +0 -0
  29. {letswork-2.0.0 → letswork-2.0.2}/server.json +0 -0
  30. {letswork-2.0.0 → letswork-2.0.2}/tests/__init__.py +0 -0
  31. {letswork-2.0.0 → letswork-2.0.2}/tests/test_auth.py +0 -0
  32. {letswork-2.0.0 → letswork-2.0.2}/tests/test_filelock.py +0 -0
  33. {letswork-2.0.0 → letswork-2.0.2}/tests/test_server.py +0 -0
  34. {letswork-2.0.0 → letswork-2.0.2}/tests/test_tunnel.py +0 -0
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: letswork
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Real-time collaborative coding via MCP — two developers, one codebase
5
5
  Author: Sai Charan Rajoju
6
6
  License-Expression: MIT
7
7
  Requires-Python: >=3.10
8
8
  Requires-Dist: click>=8.1.0
9
+ Requires-Dist: httpx>=0.27.0
9
10
  Requires-Dist: mcp[cli]>=1.0.0
10
- Requires-Dist: textual[syntax]>=0.50.0
11
- Requires-Dist: websockets>=12.0
12
11
  Provides-Extra: dev
13
12
  Requires-Dist: pytest>=7.0; extra == 'dev'
14
13
  Requires-Dist: types-requests; extra == 'dev'
@@ -15,13 +15,13 @@ def cli():
15
15
 
16
16
  @cli.command()
17
17
  @click.option("--port", default=8000, type=int, help="Port for the MCP server")
18
- def start(port):
18
+ @click.option("--debug", is_flag=True, help="Show live tool call activity")
19
+ def start(port, debug):
19
20
  """Start a LetsWork collaboration session."""
20
- from letswork.events import EventLog
21
+ import logging
22
+ from letswork.events import EventLog, EventType
21
23
  from letswork.approval import ApprovalQueue
22
- from letswork.launcher import launch_claude_code, launch_chat_window, register_guest_mcp
23
-
24
- name = click.prompt("What's your name?")
24
+ from letswork.launcher import launch_claude_code, register_guest_mcp
25
25
 
26
26
  project_root = os.getcwd()
27
27
  server_module.project_root = project_root
@@ -31,20 +31,20 @@ def start(port):
31
31
  server_module.register_user(host_token, "host")
32
32
  server_module.register_user(guest_token, "guest")
33
33
 
34
- server_module.event_log = EventLog()
34
+ event_log = EventLog()
35
+ server_module.event_log = event_log
35
36
  server_module.approval_queue = ApprovalQueue(project_root)
36
37
 
37
38
  # Start tunnel
38
39
  try:
39
40
  url, tunnel_process = start_tunnel(port)
40
41
  except RuntimeError as e:
41
- click.echo(f"Error: {e}")
42
+ click.echo(f"[ERROR] Tunnel failed: {e}")
42
43
  return
43
44
 
44
45
  mcp_url = f"{url}/mcp"
45
46
 
46
- # Silence MCP and uvicorn loggers before server starts
47
- import logging
47
+ # Silence MCP and uvicorn loggers
48
48
  for _name in ("uvicorn", "uvicorn.access", "uvicorn.error", "mcp", "mcp.server",
49
49
  "mcp.server.streamable_http", "asyncio"):
50
50
  logging.getLogger(_name).setLevel(logging.CRITICAL)
@@ -54,8 +54,7 @@ def start(port):
54
54
  and server_module.app.settings.transport_security):
55
55
  server_module.app.settings.transport_security.enable_dns_rebinding_protection = False
56
56
 
57
- # Start MCP server in background thread — run uvicorn directly so we can
58
- # pass log_config=None, which fully disables all uvicorn request logging.
57
+ # Start MCP server in background thread
59
58
  def run_server():
60
59
  import anyio
61
60
  import uvicorn
@@ -75,9 +74,10 @@ def start(port):
75
74
 
76
75
  threading.Thread(target=run_server, daemon=True).start()
77
76
 
78
- # Wait until the server is actually responding (any HTTP response means it's up)
77
+ # Wait until server is up
79
78
  import urllib.request
80
79
  import urllib.error
80
+ click.echo("[letswork] Starting MCP server...", err=True)
81
81
  for _ in range(20):
82
82
  try:
83
83
  urllib.request.urlopen(f"http://127.0.0.1:{port}/mcp", timeout=1)
@@ -85,61 +85,75 @@ def start(port):
85
85
  break # Got an HTTP response — server is up
86
86
  except Exception:
87
87
  time.sleep(0.5)
88
+ else:
89
+ click.echo("[ERROR] MCP server did not start in time. Try again.", err=True)
90
+ stop_tunnel(tunnel_process)
91
+ return
92
+
93
+ click.echo(f"[letswork] MCP server running on port {port}", err=True)
88
94
 
89
95
  # Register letswork MCP for the host (needed for approvals)
96
+ click.echo("[letswork] Registering host MCP...", err=True)
90
97
  register_guest_mcp(mcp_url, host_token)
91
98
 
92
99
  # Open Claude Code in a new Terminal window
93
100
  launch_claude_code(project_root, url, host_token)
94
101
 
95
- # Open chat window in a separate terminal (uses local URL for reliability)
96
- launch_chat_window(f"http://127.0.0.1:{port}/mcp", host_token, "host", name, project_root)
97
-
98
- # Print session info — share guest_token with collaborator
102
+ # Print session info
99
103
  click.echo("")
100
104
  click.echo("╔══════════════════════════════════════════════════╗")
101
105
  click.echo("║ 🤝 LetsWork Session Active ║")
102
106
  click.echo("║ ║")
103
- click.echo(f"║ MCP URL: {mcp_url}")
104
- click.echo(f"║ Token: {guest_token}")
107
+ click.echo(f"║ URL: {url}")
108
+ click.echo(f"║ Guest token: {guest_token}")
105
109
  click.echo("║ ║")
106
- click.echo("║ Share the URL + Token with your collaborator. ║")
107
- click.echo("║ Press Ctrl+C to stop the session. ║")
110
+ click.echo("║ Share URL + Guest token with your collaborator. ║")
111
+ click.echo("║ Press Ctrl+C to stop. ║")
108
112
  click.echo("╚══════════════════════════════════════════════════╝")
109
113
  click.echo("")
110
114
 
115
+ if debug:
116
+ click.echo("[debug] Live activity log:")
117
+
118
+ def _log_event(event):
119
+ ts = event.timestamp.strftime("%H:%M:%S")
120
+ etype = event.event_type.value
121
+ click.echo(f" [{ts}] {etype} — {event.data}")
122
+
123
+ event_log.on_event(_log_event)
124
+ else:
125
+ click.echo("Tip: run with --debug to see live tool activity.")
126
+ click.echo("")
127
+
111
128
  try:
112
129
  while True:
113
130
  time.sleep(1)
114
131
  except KeyboardInterrupt:
115
- click.echo("\nShutting down...")
132
+ click.echo("\n[letswork] Shutting down...")
116
133
  finally:
117
134
  stop_tunnel(tunnel_process)
118
- click.echo("Session ended.")
135
+ click.echo("[letswork] Session ended.")
119
136
 
120
137
 
121
138
  @cli.command()
122
139
  @click.argument("url")
123
- @click.option("--token", prompt="Enter session token", help="Secret token from the host")
140
+ @click.option("--token", prompt="Enter session token", help="Guest token from the host")
124
141
  def join(url, token):
125
142
  """Join a LetsWork session as a guest."""
126
- from letswork.launcher import register_guest_mcp, launch_guest_claude_code, launch_chat_window
127
-
128
- name = click.prompt("What's your name?")
143
+ from letswork.launcher import register_guest_mcp, launch_guest_claude_code
129
144
 
130
145
  if not url.endswith("/mcp"):
131
- url = url.rstrip("/") + "/mcp"
132
-
133
- click.echo(f"\nConnecting to {url} as {name}...")
146
+ mcp_url = url.rstrip("/") + "/mcp"
147
+ else:
148
+ mcp_url = url
134
149
 
135
- # Register MCP with Claude Code (via stdio proxy — reliable over Cloudflare)
136
- register_guest_mcp(url, token)
150
+ click.echo(f"\n[letswork] Connecting to {mcp_url}...")
137
151
 
138
- # Open Claude Code in a new Terminal window (banner shows token for reference)
139
- launch_guest_claude_code(os.getcwd(), url, token)
152
+ # Register MCP with Claude Code (via stdio proxy)
153
+ register_guest_mcp(mcp_url, token)
140
154
 
141
- # Open chat window in a separate terminal
142
- launch_chat_window(url, token, "guest", name, os.getcwd())
155
+ # Open Claude Code in a new Terminal window
156
+ launch_guest_claude_code(os.getcwd(), mcp_url, token)
143
157
 
144
158
  click.echo("✅ Claude Code is opening with LetsWork MCP connected.")
145
159
  click.echo(" You can close this terminal.")
@@ -74,8 +74,13 @@ def register_guest_mcp(url: str, token: str) -> None:
74
74
  Uses stdio transport (always works) rather than HTTP transport (unreliable
75
75
  over Cloudflare SSE). The proxy forwards tool calls to the host via HTTP.
76
76
  """
77
+ if not shutil.which("claude"):
78
+ print("⚠️ Claude Code not found. Install: npm install -g @anthropic-ai/claude-code")
79
+ return
80
+
77
81
  proxy_path = shutil.which("letswork-proxy")
78
82
  if not proxy_path:
83
+ print("⚠️ letswork-proxy not found. Try: pip install --upgrade letswork")
79
84
  return
80
85
 
81
86
  # Remove any stale entry from all scopes
@@ -105,22 +110,3 @@ def launch_guest_claude_code(project_root: str, url: str, token: str) -> None:
105
110
  print("Open a new terminal, cd to your project, and run: claude")
106
111
 
107
112
 
108
- def launch_chat_window(url: str, token: str, role: str, name: str, project_root: str) -> None:
109
- """Open a new terminal window running the LetsWork chat window."""
110
- args = (
111
- f" --url {shlex.quote(url)}"
112
- f" --token {shlex.quote(token)}"
113
- f" --role {role}"
114
- f" --name {shlex.quote(name)}"
115
- )
116
- # Prefer the installed script (resolves correctly when venv is active).
117
- # Fall back to the Python that's running this process.
118
- chat_bin = shutil.which("letswork-chat")
119
- if chat_bin:
120
- cmd = shlex.quote(chat_bin) + args
121
- else:
122
- python = shlex.quote(sys.executable)
123
- cmd = f"{python} -m letswork.tui.chat_app{args}"
124
- launched = _open_terminal(cmd, project_root)
125
- if not launched:
126
- print(f"Run in a new terminal: {cmd}")
@@ -0,0 +1,130 @@
1
+ """
2
+ LetsWork stdio MCP proxy.
3
+
4
+ Claude Code connects to this as a stdio MCP server (reliable, no streaming issues).
5
+ This proxy forwards all tool calls to the host's HTTP MCP server using a proper
6
+ MCP client session (required by FastMCP's streamable HTTP transport).
7
+
8
+ Usage (done automatically by `letswork join`):
9
+ claude mcp add letswork -- letswork-proxy --url <URL> --token <TOKEN>
10
+ """
11
+ import sys
12
+ import asyncio
13
+ import argparse
14
+ import logging
15
+ from mcp.server import Server
16
+ from mcp.server.stdio import stdio_server
17
+ from mcp.client.streamable_http import streamablehttp_client
18
+ from mcp import ClientSession, types
19
+
20
+ log = logging.getLogger("letswork.proxy")
21
+
22
+
23
+ def _setup_logging(debug: bool) -> None:
24
+ level = logging.DEBUG if debug else logging.WARNING
25
+ logging.basicConfig(
26
+ stream=sys.stderr,
27
+ level=level,
28
+ format="[proxy %(levelname)s] %(message)s",
29
+ )
30
+
31
+
32
+ def make_proxy_server(base_url: str, token: str) -> tuple:
33
+ """Create an MCP Server that forwards calls to the remote host via a proper session."""
34
+ url = base_url.rstrip("/")
35
+ if not url.endswith("/mcp"):
36
+ url = url + "/mcp"
37
+
38
+ server = Server("letswork-proxy")
39
+ # Shared session state — populated once the client connects
40
+ _session: ClientSession | None = None
41
+
42
+ async def _get_session() -> ClientSession:
43
+ nonlocal _session
44
+ if _session is None:
45
+ raise RuntimeError("Not connected to host")
46
+ return _session
47
+
48
+ @server.list_tools()
49
+ async def list_tools() -> list[types.Tool]:
50
+ session = await _get_session()
51
+ result = await session.list_tools()
52
+ tools = []
53
+ for t in result.tools:
54
+ schema = t.inputSchema if t.inputSchema else {"type": "object", "properties": {}}
55
+ # Strip 'token' — proxy injects it automatically
56
+ schema = dict(schema)
57
+ props = dict(schema.get("properties", {}))
58
+ props.pop("token", None)
59
+ schema["properties"] = props
60
+ required = [r for r in schema.get("required", []) if r != "token"]
61
+ if required:
62
+ schema["required"] = required
63
+ elif "required" in schema:
64
+ del schema["required"]
65
+ tools.append(types.Tool(
66
+ name=t.name,
67
+ description=t.description or "",
68
+ inputSchema=schema,
69
+ ))
70
+ return tools
71
+
72
+ @server.call_tool()
73
+ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
74
+ session = await _get_session()
75
+ # Inject token automatically
76
+ arguments = {**arguments, "token": token}
77
+ log.debug(f"→ tool call: {name}({list(k for k in arguments if k != 'token')})")
78
+ try:
79
+ result = await session.call_tool(name, arguments)
80
+ except Exception as e:
81
+ log.error(f"✗ tool call {name} failed: {e}")
82
+ raise
83
+ out = []
84
+ for item in result.content:
85
+ if item.type == "text":
86
+ out.append(types.TextContent(type="text", text=item.text))
87
+ if not out:
88
+ out.append(types.TextContent(type="text", text=str(result)))
89
+ log.debug(f"← {name} OK")
90
+ return out
91
+
92
+ async def run(read_stream, write_stream):
93
+ nonlocal _session
94
+ log.debug(f"Connecting to host at {url}")
95
+ try:
96
+ async with streamablehttp_client(url) as (host_read, host_write, _):
97
+ async with ClientSession(host_read, host_write) as session:
98
+ await session.initialize()
99
+ _session = session
100
+ log.debug("Connected to host MCP server")
101
+ await server.run(
102
+ read_stream, write_stream,
103
+ server.create_initialization_options(),
104
+ )
105
+ except Exception as e:
106
+ log.error(f"Proxy connection failed: {e}")
107
+ raise
108
+
109
+ return server, run
110
+
111
+
112
+ async def _main(url: str, token: str, debug: bool) -> None:
113
+ _setup_logging(debug)
114
+ log.debug(f"Starting proxy → {url}")
115
+ _server, run = make_proxy_server(url, token)
116
+ async with stdio_server() as (read_stream, write_stream):
117
+ await run(read_stream, write_stream)
118
+
119
+
120
+ def main() -> None:
121
+ parser = argparse.ArgumentParser(description="LetsWork MCP stdio proxy")
122
+ parser.add_argument("--url", required=True, help="Host MCP URL")
123
+ parser.add_argument("--token", required=True, help="Session token")
124
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
125
+ args = parser.parse_args()
126
+ asyncio.run(_main(args.url, args.token, args.debug))
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -111,14 +111,5 @@ class RemoteClient:
111
111
  def unlock_file(self, path: str) -> str:
112
112
  return self._call_tool("unlock_file", {"token": self.token, "path": path})
113
113
 
114
- def register_name(self, display_name: str) -> str:
115
- return self._call_tool("register_name", {"token": self.token, "display_name": display_name})
116
-
117
- def send_message(self, message: str) -> str:
118
- return self._call_tool("send_message", {"token": self.token, "message": message})
119
-
120
- def get_events(self, since_index: int = 0) -> str:
121
- return self._call_tool("get_events", {"token": self.token, "since_index": since_index})
122
-
123
114
  def get_status(self) -> str:
124
115
  return self._call_tool("get_status", {"token": self.token})
@@ -37,6 +37,14 @@ def safe_resolve(path: str, root: str) -> str:
37
37
  raise ValueError("Access denied: path outside project directory")
38
38
  return resolved
39
39
 
40
+ @app.tool()
41
+ def ping(token: str) -> str:
42
+ if not check_auth(token):
43
+ raise ValueError("Unauthorized: invalid token")
44
+ user_id = get_user(token)
45
+ return f"pong — connected to {project_root} as {user_id}"
46
+
47
+
40
48
  @app.tool()
41
49
  def list_files(token: str, path: str = ".") -> str:
42
50
  if not check_auth(token):
@@ -207,43 +215,6 @@ def get_status(token: str) -> str:
207
215
  return "\n".join(status_lines)
208
216
 
209
217
 
210
- @app.tool()
211
- def register_name(token: str, display_name: str) -> str:
212
- if not check_auth(token):
213
- raise ValueError("Unauthorized: invalid token")
214
- user_id = get_user(token)
215
- token_to_user[token] = display_name
216
- event_log.emit(EventType.CONNECTION, display_name, {})
217
- return f"Registered as {display_name} (was {user_id})"
218
-
219
-
220
- @app.tool()
221
- def send_message(token: str, message: str) -> str:
222
- if not check_auth(token):
223
- raise ValueError("Unauthorized: invalid token")
224
- user_id = get_user(token)
225
- if not message.strip():
226
- raise ValueError("Message cannot be empty")
227
- event_log.emit(EventType.CHAT_MESSAGE, user_id, {"message": message})
228
- return f"Message sent by {user_id}"
229
-
230
-
231
- @app.tool()
232
- def get_events(token: str, since_index: int = 0) -> str:
233
- if not check_auth(token):
234
- raise ValueError("Unauthorized: invalid token")
235
- if since_index >= len(event_log._events):
236
- return "no_new_events"
237
-
238
- new_events = event_log._events[since_index:]
239
- result_lines = []
240
- for event in new_events:
241
- result_lines.append(event.message)
242
-
243
- result_lines.append(f"__INDEX__:{len(event_log._events)}")
244
- return "\n".join(result_lines)
245
-
246
-
247
218
  @app.tool()
248
219
  def get_pending_changes(token: str) -> str:
249
220
  if not check_auth(token):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "letswork"
7
- version = "2.0.0"
7
+ version = "2.0.2"
8
8
  description = "Real-time collaborative coding via MCP — two developers, one codebase"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -13,14 +13,12 @@ authors = [{ name = "Sai Charan Rajoju" }]
13
13
  dependencies = [
14
14
  "mcp[cli]>=1.0.0",
15
15
  "click>=8.1.0",
16
- "textual[syntax]>=0.50.0",
17
- "websockets>=12.0"
16
+ "httpx>=0.27.0",
18
17
  ]
19
18
 
20
19
  [project.scripts]
21
20
  letswork = "letswork.cli:cli"
22
21
  letswork-proxy = "letswork.proxy:main"
23
- letswork-chat = "letswork.tui.chat_app:main"
24
22
 
25
23
  [tool.hatch.build.targets.wheel]
26
24
  packages = ["letswork"]
@@ -1,109 +0,0 @@
1
- """
2
- LetsWork stdio MCP proxy.
3
-
4
- Claude Code connects to this as a stdio MCP server (reliable, no streaming issues).
5
- This proxy forwards all tool calls to the host's HTTP MCP server over Cloudflare.
6
-
7
- Usage (done automatically by `letswork join`):
8
- claude mcp add letswork -- letswork-proxy --url <URL> --token <TOKEN>
9
- """
10
- import sys
11
- import asyncio
12
- import argparse
13
- import httpx
14
- from mcp.server import Server
15
- from mcp.server.stdio import stdio_server
16
- from mcp import types
17
-
18
-
19
- def make_proxy_server(base_url: str, token: str) -> Server:
20
- """Create an MCP Server that forwards calls to the remote host."""
21
- # Ensure URL ends with /mcp
22
- url = base_url.rstrip("/")
23
- if not url.endswith("/mcp"):
24
- url = url + "/mcp"
25
-
26
- server = Server("letswork-proxy")
27
-
28
- async def _http_post(payload: dict) -> dict:
29
- async with httpx.AsyncClient(timeout=30) as client:
30
- r = await client.post(
31
- url,
32
- json=payload,
33
- headers={
34
- "Content-Type": "application/json",
35
- "Accept": "application/json, text/event-stream",
36
- },
37
- )
38
- r.raise_for_status()
39
- # Parse SSE response: find 'data: {...}' line
40
- for line in r.text.splitlines():
41
- if line.startswith("data: "):
42
- import json
43
- return json.loads(line[6:])
44
- raise RuntimeError("No data in response")
45
-
46
- @server.list_tools()
47
- async def list_tools() -> list[types.Tool]:
48
- resp = await _http_post({
49
- "jsonrpc": "2.0", "id": 1,
50
- "method": "tools/list", "params": {},
51
- })
52
- tools = []
53
- for t in resp.get("result", {}).get("tools", []):
54
- schema = t.get("inputSchema", {"type": "object", "properties": {}})
55
- # Strip 'token' from schema — proxy injects it automatically
56
- schema = dict(schema)
57
- props = dict(schema.get("properties", {}))
58
- props.pop("token", None)
59
- schema["properties"] = props
60
- required = [r for r in schema.get("required", []) if r != "token"]
61
- if required:
62
- schema["required"] = required
63
- elif "required" in schema:
64
- del schema["required"]
65
- tools.append(types.Tool(
66
- name=t["name"],
67
- description=t.get("description", ""),
68
- inputSchema=schema,
69
- ))
70
- return tools
71
-
72
- @server.call_tool()
73
- async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
74
- # Inject token automatically so guest doesn't have to think about it
75
- arguments = {**arguments, "token": token}
76
- resp = await _http_post({
77
- "jsonrpc": "2.0", "id": 1,
78
- "method": "tools/call",
79
- "params": {"name": name, "arguments": arguments},
80
- })
81
- result = resp.get("result", {})
82
- content = result.get("content", [])
83
- out = []
84
- for item in content:
85
- if item.get("type") == "text":
86
- out.append(types.TextContent(type="text", text=item["text"]))
87
- if not out:
88
- out.append(types.TextContent(type="text", text=str(result)))
89
- return out
90
-
91
- return server
92
-
93
-
94
- async def _run(url: str, token: str) -> None:
95
- server = make_proxy_server(url, token)
96
- async with stdio_server() as (read_stream, write_stream):
97
- await server.run(read_stream, write_stream, server.create_initialization_options())
98
-
99
-
100
- def main() -> None:
101
- parser = argparse.ArgumentParser(description="LetsWork MCP stdio proxy")
102
- parser.add_argument("--url", required=True, help="Host MCP URL")
103
- parser.add_argument("--token", required=True, help="Session token")
104
- args = parser.parse_args()
105
- asyncio.run(_run(args.url, args.token))
106
-
107
-
108
- if __name__ == "__main__":
109
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes