66AVAILABLE TOOLS (actions you can perform):
77- exec: Run shell commands, scripts, and programs in the project
88 Use for: running code, data processing, build/test commands, git operations, etc.
9+ - jupyter_execute: Execute code using Jupyter kernels (Python, R, Julia, etc.)
10+ Use for: interactive code execution, data analysis, visualization, scientific computing
911
1012AVAILABLE RESOURCES (information you can read):
1113- project-files: Browse the project file structure
3335
3436import os
3537import sys
38+ import time
3639from typing import Optional
3740
3841from mcp .server .fastmcp import FastMCP
3942
4043from cocalc_api import Project , Hub
4144
4245
46+ def _retry_with_backoff (func , max_retries : int = 3 , retry_delay : int = 2 ):
47+ """
48+ Retry a function with exponential backoff for transient failures.
49+
50+ Used during server initialization for operations that may timeout on cold starts.
51+ """
52+ for attempt in range (max_retries ):
53+ try :
54+ return func ()
55+ except Exception as e :
56+ error_msg = str (e ).lower ()
57+ is_retryable = any (
58+ keyword in error_msg
59+ for keyword in ["timeout" , "closed" , "connection" , "reset" , "broken" ]
60+ )
61+ if is_retryable and attempt < max_retries - 1 :
62+ print (
63+ f"Initialization attempt { attempt + 1 } failed ({ error_msg [:50 ]} ...), "
64+ f"retrying in { retry_delay } s..." ,
65+ file = sys .stderr ,
66+ )
67+ time .sleep (retry_delay )
68+ else :
69+ raise
70+
71+
4372def get_config () -> tuple [str , str , Optional [str ]]:
4473 """
4574 Get and validate MCP server configuration.
@@ -106,6 +135,7 @@ def check_api_key_scope(api_key: str, host: str) -> dict[str, str]:
106135This server gives you direct access to a CoCalc project or account environment through the Model Context Protocol (MCP).
107136
108137WHAT YOU CAN DO:
138+ - Execute code using Jupyter kernels (Python, R, Julia, etc.) with rich output and visualization support
109139- Execute arbitrary shell commands in a Linux environment with Python, Node.js, R, Julia, and 100+ tools
110140- Browse and explore project files to understand structure and contents
111141- Run code, scripts, and build/test commands
@@ -114,11 +144,14 @@ def check_api_key_scope(api_key: str, host: str) -> dict[str, str]:
114144
115145HOW TO USE:
1161461. Start by exploring the project structure using the project-files resource
117- 2. Use the exec tool to run commands, scripts, or programs
118- 3. Combine multiple commands to accomplish complex workflows
147+ 2. Use jupyter_execute for interactive code execution with rich output (plots, tables, etc.)
148+ 3. Use exec tool to run shell commands, scripts, or programs
149+ 4. Combine multiple commands to accomplish complex workflows
119150
120151EXAMPLES:
121- - Execute Python: exec with command="python3 script.py --verbose"
152+ - Execute Python interactively: jupyter_execute with input="import pandas as pd; df = pd.read_csv('data.csv'); df.describe()"
153+ - Data visualization: jupyter_execute with input="import matplotlib.pyplot as plt; plt.plot([1,2,3]); plt.show()"
154+ - Execute shell command: exec with command="python3 script.py --verbose"
122155- List files: use project-files resource or exec with command="ls -la"
123156- Run tests: exec with command="pytest tests/" bash=true
124157- Git operations: exec with command="git log --oneline" in your repository
@@ -157,22 +190,12 @@ def _initialize_config() -> None:
157190 try :
158191 _api_key_scope = check_api_key_scope (_api_key , _host )
159192 except RuntimeError as check_error :
160- # If it's a project-scoped key error, try the project API to discover the project_id
193+ # If it's a project-scoped key error, use a placeholder project_id
194+ # Project-scoped keys have the project_id embedded in the key itself
161195 if "project-scoped" in str (check_error ):
162- try :
163- # Try with empty project_id - project-scoped keys will use their own
164- project = Project (api_key = _api_key , project_id = "" , host = _host )
165- result = project .system .ping ()
166- # Check if the response includes project_id (it shouldn't from ping, but try anyway)
167- if isinstance (result , dict ) and "project_id" in result :
168- _api_key_scope = {"project_id" : result ["project_id" ]}
169- else :
170- # If we still don't have it, this is an error
171- raise RuntimeError ("Could not determine project_id from project-scoped API key. "
172- "Please restart with COCALC_PROJECT_ID environment variable." )
173- except Exception as project_error :
174- raise RuntimeError (f"Project-scoped API key detected but could not determine project_id. "
175- f"Error: { project_error } " ) from project_error
196+ # Use empty string as project_id - the Project client will extract it from the API key
197+ _api_key_scope = {"project_id" : "" }
198+ print ("✓ Connected with project-scoped API key" , file = sys .stderr )
176199 else :
177200 raise
178201
@@ -181,12 +204,15 @@ def _initialize_config() -> None:
181204 print (f"✓ Connected with account-scoped API key (account: { account_id } )" , file = sys .stderr )
182205 elif "project_id" in _api_key_scope :
183206 project_id = _api_key_scope ["project_id" ]
184- if not project_id :
185- raise RuntimeError ("Project ID not found for project-scoped API key" )
186- print (f"✓ Connected with project-scoped API key (project: { project_id } )" , file = sys .stderr )
187- # For project-scoped keys, eagerly create the project client
188- client = Project (api_key = _api_key , project_id = project_id , host = _host )
189- _project_clients [project_id ] = client
207+ # For project-scoped keys with empty/None project_id, the Project client will extract it from the API key
208+ if project_id :
209+ print (f"✓ Connected with project-scoped API key (project: { project_id } )" , file = sys .stderr )
210+ # For project-scoped keys, eagerly create the project client
211+ client = Project (api_key = _api_key , project_id = project_id , host = _host )
212+ _project_clients [project_id ] = client
213+ else :
214+ # Project-scoped key with empty project_id - will be discovered on first use
215+ print ("✓ Connected with project-scoped API key (project ID will be discovered on first use)" , file = sys .stderr )
190216 else :
191217 # If we got here with no project_id but it might be project-scoped, check if COCALC_PROJECT_ID was provided
192218 if project_id_config :
@@ -232,19 +258,24 @@ def get_project_client(project_id: Optional[str] = None) -> Project:
232258 raise RuntimeError ("Account-scoped API key requires an explicit project_id argument. "
233259 "No project_id provided to get_project_client()." )
234260
235- if not project_id :
236- raise RuntimeError ("Project ID cannot be empty" )
261+ # For project-scoped keys with None/empty project_id, the Project client will extract it from the API key
262+ # For account-scoped keys, project_id must be non-empty
263+ if not project_id and _api_key_scope and "account_id" in _api_key_scope :
264+ raise RuntimeError ("Account-scoped API key requires a non-empty project_id" )
265+
266+ # Use a cache key that handles None/empty project_id for project-scoped keys
267+ cache_key = project_id if project_id else "_default_project"
237268
238269 # Return cached client if available
239- if project_id in _project_clients :
240- return _project_clients [project_id ]
270+ if cache_key in _project_clients :
271+ return _project_clients [cache_key ]
241272
242273 # Create new project client
243274 # At this point, _api_key and _host are guaranteed to be non-None (set in _initialize_config)
244275 assert _api_key is not None
245276 assert _host is not None
246277 client = Project (api_key = _api_key , project_id = project_id , host = _host )
247- _project_clients [project_id ] = client
278+ _project_clients [cache_key ] = client
248279 return client
249280
250281
0 commit comments