Skip to content

Commit 006e8b9

Browse files
committed
cocalc-api/mcp: refine authentication
1 parent 01cae0a commit 006e8b9

File tree

15 files changed

+356
-82
lines changed

15 files changed

+356
-82
lines changed

src/packages/conat/hub/api/system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { type UserSearchResult } from "@cocalc/util/db-schema/accounts";
99
export const system = {
1010
getCustomize: noAuth,
1111
ping: noAuth,
12+
test: authFirst,
1213
terminate: authFirst,
1314
userTracking: authFirst,
1415
logClientError: authFirst,
@@ -31,6 +32,8 @@ export interface System {
3132
getCustomize: (fields?: string[]) => Promise<Customize>;
3233
// ping server and get back the current time
3334
ping: () => { now: number };
35+
// test API key and return scope information (account_id or project_id) and server time
36+
test: () => Promise<{ account_id?: string; project_id?: string; server_time: number }>;
3437
// terminate a service:
3538
// - only admin can do this.
3639
// - useful for development

src/packages/conat/project/api/system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const system = {
2828
configuration: true,
2929

3030
ping: true,
31+
test: true,
3132
exec: true,
3233

3334
signal: true,
@@ -69,6 +70,8 @@ export interface System {
6970

7071
ping: () => Promise<{ now: number }>;
7172

73+
test: () => Promise<{ project_id: string; server_time: number }>;
74+
7275
exec: (opts: ExecuteCodeOptions) => Promise<ExecuteCodeOutput>;
7376

7477
signal: (opts: {

src/packages/next/pages/api/conat/project.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export default async function handle(req, res) {
5858
args,
5959
timeout,
6060
});
61+
// For project-scoped API keys, include the project_id in the response
62+
// so the client can discover it
63+
if (project_id0 && !resp.project_id) {
64+
resp.project_id = project_id0;
65+
}
6166
res.json(resp);
6267
} catch (err) {
6368
res.json({ error: err.message });

src/packages/project/conat/api/system.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ export async function ping() {
22
return { now: Date.now() };
33
}
44

5+
export async function test({
6+
project_id,
7+
}: {
8+
project_id?: string;
9+
} = {}) {
10+
return {
11+
project_id: project_id ?? "",
12+
server_time: Date.now(),
13+
};
14+
}
15+
516
export async function terminate() {}
617

718
import { handleExecShellCode } from "@cocalc/project/exec_shell_code";

src/packages/server/api/project-bridge.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ async function callProject({
5151
service: "api",
5252
});
5353
try {
54-
const data = { name, args };
54+
// For system.test(), inject project_id into args[0] if not already present
55+
let finalArgs = args;
56+
if (name === "system.test" && (!args || args.length === 0)) {
57+
finalArgs = [{ project_id }];
58+
}
59+
const data = { name, args: finalArgs };
5560
// we use waitForInterest because often the project hasn't
5661
// quite fully started.
5762
const resp = await client.request(subject, data, {

src/packages/server/conat/api/system.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ export function ping() {
1818
return { now: Date.now() };
1919
}
2020

21+
export async function test({
22+
account_id,
23+
project_id,
24+
}: { account_id?: string; project_id?: string } = {}) {
25+
// Return API key scope information and server time
26+
// The authFirst decorator determines the scope from the API key and injects
27+
// either account_id (for account-scoped keys) or project_id (for project-scoped keys)
28+
// into this parameter object.
29+
const response: { account_id?: string; project_id?: string; server_time: number } = {
30+
server_time: Date.now(),
31+
};
32+
if (account_id) {
33+
response.account_id = account_id;
34+
}
35+
if (project_id) {
36+
response.project_id = project_id;
37+
}
38+
return response;
39+
}
40+
2141
export async function terminate() {}
2242

2343
export async function userTracking({

src/python/cocalc-api/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ help:
1111
@echo " coverage - Run tests with coverage reporting (HTML + terminal)"
1212
@echo " coverage-report - Show coverage report in terminal"
1313
@echo " coverage-html - Generate HTML coverage report only"
14-
@echo " mcp - Start the MCP server (requires COCALC_API_KEY, COCALC_PROJECT_ID, and COCALC_HOST)"
14+
@echo " mcp - Start the MCP server (requires COCALC_API_KEY)"
1515
@echo " serve-docs - Serve documentation locally"
1616
@echo " build-docs - Build documentation"
1717
@echo " publish - Build and publish package"

src/python/cocalc-api/src/cocalc_api/api_types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ class PingResponse(TypedDict):
55
now: int
66

77

8+
class TestResponse(TypedDict, total=False):
9+
account_id: Optional[str]
10+
project_id: Optional[str]
11+
server_time: int
12+
13+
814
class ExecuteCodeOutput(TypedDict):
915
stdout: str
1016
stderr: str

src/python/cocalc-api/src/cocalc_api/hub.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import httpx
22
from typing import Any, Literal, Optional
33
from .util import api_method, handle_error
4-
from .api_types import PingResponse, UserSearchResult, MessageType
4+
from .api_types import PingResponse, TestResponse, UserSearchResult, MessageType
55
from .org import Organizations
66

77

@@ -83,6 +83,19 @@ def ping(self) -> PingResponse:
8383
"""
8484
raise NotImplementedError
8585

86+
@api_method("system.test")
87+
def test(self) -> TestResponse:
88+
"""
89+
Test the API key and get its scope information.
90+
91+
Returns:
92+
TestResponse: JSON object containing:
93+
- account_id (if account-scoped key)
94+
- project_id (if project-scoped key)
95+
- server_time (current server time in milliseconds since epoch)
96+
"""
97+
raise NotImplementedError
98+
8699
def get_names(self, account_ids: list[str]) -> list[str]:
87100
"""
88101
Get the displayed names of CoCalc accounts with given IDs.

src/python/cocalc-api/src/cocalc_api/mcp/DEVELOPMENT.md

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,102 @@ Learn more: https://modelcontextprotocol.io/docs/getting-started/intro
1010

1111
### Required Environment Variables
1212

13-
- **`COCALC_API_KEY`** - API key for CoCalc authentication (format: `sk-...`)
14-
- **`COCALC_PROJECT_ID`** - UUID of the target CoCalc project
13+
- **`COCALC_API_KEY`** - API key for CoCalc authentication (format: `sk-...`, can be account-scoped or project-scoped)
1514
- **`COCALC_HOST`** (optional) - CoCalc instance URL (default: `https://cocalc.com`)
1615

1716
### Setup Examples
1817

19-
**Local Development:**
18+
**Local Development (Recommended: Project-Scoped API Key):**
19+
20+
Create a project-scoped API key in your CoCalc project settings:
21+
22+
```bash
23+
export COCALC_API_KEY="sk-your-project-api-key-here"
24+
export COCALC_HOST="http://localhost:5000" # For local CoCalc, or omit for cocalc.com
25+
uv run cocalc-mcp-server
26+
```
27+
28+
When started, the server will report:
29+
30+
```
31+
✓ Connected with project-scoped API key (project: 6e75dbf1-0342-4249-9dce-6b21648656e9)
32+
```
33+
34+
**Alternative: Account-Scoped API Key:**
35+
36+
Create an account-scoped API key in your CoCalc account settings (Settings → API keys):
37+
2038
```bash
21-
export COCALC_API_KEY="sk-your-api-key-here"
22-
export COCALC_PROJECT_ID="6e75dbf1-0342-4249-9dce-6b21648656e9"
23-
export COCALC_HOST="http://localhost:5000" # For local CoCalc
39+
export COCALC_API_KEY="sk-your-account-api-key-here"
2440
uv run cocalc-mcp-server
2541
```
2642

27-
**Claude Code CLI:**
43+
When started, the server will report:
44+
45+
```
46+
✓ Connected with account-scoped API key (account: d0bdabfd-850e-4c8d-8510-f6f1ecb9a5eb)
47+
```
48+
49+
**Claude Code CLI (Project-Scoped Key - Recommended):**
50+
2851
```bash
2952
claude mcp add \
3053
--transport stdio \
3154
cocalc \
32-
--env COCALC_API_KEY="sk-your-api-key-here" \
33-
--env COCALC_PROJECT_ID="6e75dbf1-0342-4249-9dce-6b21648656e9" \
34-
--env COCALC_HOST="http://localhost:5000" \
55+
--env COCALC_API_KEY="sk-your-project-api-key-here" \
3556
-- uv --directory /path/to/cocalc-api run cocalc-mcp-server
3657
```
3758

38-
**Claude Desktop:**
59+
**Claude Code CLI (Account-Scoped Key):**
60+
61+
```bash
62+
claude mcp add \
63+
--transport stdio \
64+
cocalc \
65+
--env COCALC_API_KEY="sk-your-account-api-key-here" \
66+
-- uv --directory /path/to/cocalc-api run cocalc-mcp-server
67+
```
68+
69+
**Claude Desktop (Project-Scoped Key - Recommended):**
70+
3971
Add to `~/.config/Claude/claude_desktop_config.json`:
72+
4073
```json
4174
{
4275
"mcpServers": {
4376
"cocalc": {
4477
"command": "uv",
45-
"args": ["--directory", "/path/to/cocalc-api", "run", "cocalc-mcp-server"],
78+
"args": [
79+
"--directory",
80+
"/path/to/cocalc-api",
81+
"run",
82+
"cocalc-mcp-server"
83+
],
4684
"env": {
47-
"COCALC_API_KEY": "sk-your-api-key-here",
48-
"COCALC_PROJECT_ID": "6e75dbf1-0342-4249-9dce-6b21648656e9",
49-
"COCALC_HOST": "http://localhost:5000"
85+
"COCALC_API_KEY": "sk-your-project-api-key-here"
86+
}
87+
}
88+
}
89+
}
90+
```
91+
92+
**Claude Desktop (Account-Scoped Key):**
93+
94+
Add to `~/.config/Claude/claude_desktop_config.json`:
95+
96+
```json
97+
{
98+
"mcpServers": {
99+
"cocalc": {
100+
"command": "uv",
101+
"args": [
102+
"--directory",
103+
"/path/to/cocalc-api",
104+
"run",
105+
"cocalc-mcp-server"
106+
],
107+
"env": {
108+
"COCALC_API_KEY": "sk-your-account-api-key-here"
50109
}
51110
}
52111
}
@@ -97,6 +156,7 @@ src/cocalc_api/mcp/
97156
Execute arbitrary shell commands in the CoCalc project.
98157

99158
**Parameters:**
159+
100160
- `command` (string, required): Command to execute
101161
- `args` (list, optional): Command arguments
102162
- `bash` (boolean, optional): Interpret as bash script
@@ -106,6 +166,7 @@ Execute arbitrary shell commands in the CoCalc project.
106166
**Returns:** stdout, stderr, and exit_code
107167

108168
**Examples:**
169+
109170
```json
110171
{"command": "echo 'Hello'"}
111172
{"command": "python", "args": ["script.py", "--verbose"]}
@@ -121,6 +182,7 @@ Browse project directory structure.
121182
**URI:** `cocalc://project-files/{path}`
122183

123184
**Parameters:**
185+
124186
- `path` (string, optional): Directory path (default: `.`)
125187

126188
**Returns:** Formatted list of files and directories
@@ -130,6 +192,7 @@ Browse project directory structure.
130192
### Adding a New Tool
131193

132194
1. Create `tools/my_tool.py`:
195+
133196
```python
134197
def register_my_tool(mcp) -> None:
135198
"""Register my tool with FastMCP instance."""
@@ -143,6 +206,7 @@ def register_my_tool(mcp) -> None:
143206
```
144207

145208
2. Update `tools/__init__.py`:
209+
146210
```python
147211
def register_tools(mcp) -> None:
148212
from .exec import register_exec_tool

0 commit comments

Comments
 (0)