cooperage-sdk 0.1.0__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.
- cooperage_sdk-0.1.0/.gitignore +1 -0
- cooperage_sdk-0.1.0/PKG-INFO +8 -0
- cooperage_sdk-0.1.0/README.md +111 -0
- cooperage_sdk-0.1.0/cooperage_sdk/__init__.py +24 -0
- cooperage_sdk-0.1.0/cooperage_sdk/docs.py +56 -0
- cooperage_sdk-0.1.0/cooperage_sdk/server.py +19 -0
- cooperage_sdk-0.1.0/cooperage_sdk/workspace.py +55 -0
- cooperage_sdk-0.1.0/pyproject.toml +17 -0
- cooperage_sdk-0.1.0/uv.lock +765 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__pycache__/
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Cooperage SDK
|
|
2
|
+
|
|
3
|
+
Lightweight helpers for writing [Cooperage](https://github.com/cooperage-io/cooperage)-compatible MCP servers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cooperage-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
from cooperage_sdk import workspace, serve, register_docs
|
|
16
|
+
|
|
17
|
+
mcp = FastMCP("my-server", json_response=True, stateless_http=True)
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
def process_data(input_file: str, output_file: str) -> str:
|
|
21
|
+
"""Process a data file from the workspace and write results."""
|
|
22
|
+
data = workspace.path(input_file).read_text()
|
|
23
|
+
workspace.path(output_file).write_text(data.upper())
|
|
24
|
+
return f"Processed {input_file} → {output_file}"
|
|
25
|
+
|
|
26
|
+
register_docs(mcp) # expose docs/ directory as MCP Resources
|
|
27
|
+
serve(mcp)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### workspace
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from cooperage_sdk import workspace
|
|
36
|
+
|
|
37
|
+
# Get safe paths — blocks traversal, hides the env var
|
|
38
|
+
workspace.path("file.txt") # returns a Path
|
|
39
|
+
workspace.path("file.txt").read_text() # read
|
|
40
|
+
workspace.path("out.txt").write_text() # write
|
|
41
|
+
|
|
42
|
+
# Works with any library
|
|
43
|
+
Image.open(workspace.path("photo.png"))
|
|
44
|
+
pd.read_csv(workspace.path("data.csv"))
|
|
45
|
+
plt.savefig(workspace.path("chart.png"))
|
|
46
|
+
|
|
47
|
+
# Helpers
|
|
48
|
+
workspace.exists("file.txt") # check existence
|
|
49
|
+
workspace.list() # list all files
|
|
50
|
+
workspace.list("subdir") # list files in subdir
|
|
51
|
+
workspace.root # raw Path to /workspace
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### serve
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from cooperage_sdk import serve
|
|
58
|
+
|
|
59
|
+
serve(mcp) # start on port 8000
|
|
60
|
+
serve(mcp, port=9000) # custom port
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### register_docs
|
|
64
|
+
|
|
65
|
+
Expose a `docs/` directory as MCP Resources so the LLM can discover and read
|
|
66
|
+
server documentation on demand.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from cooperage_sdk import register_docs
|
|
70
|
+
|
|
71
|
+
register_docs(mcp) # scans docs/ directory (default)
|
|
72
|
+
register_docs(mcp, "manuals") # custom directory
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Each file in the directory becomes an MCP Resource:
|
|
76
|
+
- **URI**: `docs://<filename>` (e.g. `docs://quickstart.md`)
|
|
77
|
+
- **Name**: derived from filename (e.g. "Quickstart")
|
|
78
|
+
- **Description**: first line of the file — lets the LLM preview contents
|
|
79
|
+
before reading
|
|
80
|
+
|
|
81
|
+
#### Example
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
my-server/
|
|
85
|
+
server.py
|
|
86
|
+
Dockerfile
|
|
87
|
+
docs/
|
|
88
|
+
quickstart.md "Getting started with the image analyzer"
|
|
89
|
+
scene-types.md "Supported scene types: terrain, urban, coastal"
|
|
90
|
+
api-reference.md "Full API reference for all tools"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The LLM sees:
|
|
94
|
+
```
|
|
95
|
+
cooperage_list_server_resources("session-1", "my-server")
|
|
96
|
+
→ [
|
|
97
|
+
{"uri": "docs://quickstart.md", "name": "Quickstart", "description": "Getting started with the image analyzer"},
|
|
98
|
+
{"uri": "docs://scene-types.md", "name": "Scene Types", "description": "Supported scene types: terrain, urban, coastal"},
|
|
99
|
+
{"uri": "docs://api-reference.md", "name": "Api Reference", "description": "Full API reference for all tools"},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
cooperage_read_server_resource("session-1", "my-server", "docs://scene-types.md")
|
|
103
|
+
→ (full markdown content)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The LLM reads the descriptions, decides which docs are relevant, and only
|
|
107
|
+
pulls what it needs — no wasted context tokens.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cooperage SDK — lightweight helpers for writing Cooperage-compatible MCP servers.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
from cooperage_sdk import workspace, serve, register_docs
|
|
7
|
+
|
|
8
|
+
mcp = FastMCP("my-server", json_response=True, stateless_http=True)
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
def my_tool(input_file: str) -> str:
|
|
12
|
+
data = workspace.path(input_file).read_text()
|
|
13
|
+
workspace.path("output.txt").write_text(data.upper())
|
|
14
|
+
return "Done"
|
|
15
|
+
|
|
16
|
+
register_docs(mcp)
|
|
17
|
+
serve(mcp)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from cooperage_sdk.workspace import workspace
|
|
21
|
+
from cooperage_sdk.server import serve
|
|
22
|
+
from cooperage_sdk.docs import register_docs
|
|
23
|
+
|
|
24
|
+
__all__ = ["workspace", "serve", "register_docs"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Helpers for exposing server documentation as MCP Resources."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_docs(mcp, docs_dir: str = "docs") -> None:
|
|
7
|
+
"""Register all markdown files in a directory as MCP Resources.
|
|
8
|
+
|
|
9
|
+
Each file becomes a resource with:
|
|
10
|
+
- URI: docs://<filename>
|
|
11
|
+
- Name: derived from filename (e.g. "scene-types.md" → "Scene Types")
|
|
12
|
+
- Description: first non-empty line of the file
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from cooperage_sdk import register_docs
|
|
16
|
+
|
|
17
|
+
mcp = FastMCP("my-server", json_response=True, stateless_http=True)
|
|
18
|
+
register_docs(mcp)
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
mcp: A FastMCP instance.
|
|
22
|
+
docs_dir: Path to the docs directory (default: "docs").
|
|
23
|
+
"""
|
|
24
|
+
docs_path = Path(docs_dir)
|
|
25
|
+
if not docs_path.is_dir():
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
for filepath in sorted(docs_path.rglob("*")):
|
|
29
|
+
if not filepath.is_file():
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
relative = str(filepath.relative_to(docs_path))
|
|
33
|
+
uri = f"docs://{relative}"
|
|
34
|
+
content = filepath.read_text()
|
|
35
|
+
|
|
36
|
+
# Derive a human-readable name from the filename
|
|
37
|
+
name = filepath.stem.replace("-", " ").replace("_", " ").title()
|
|
38
|
+
|
|
39
|
+
# Use first non-empty line as description
|
|
40
|
+
description = ""
|
|
41
|
+
for line in content.splitlines():
|
|
42
|
+
stripped = line.strip().lstrip("#").strip()
|
|
43
|
+
if stripped:
|
|
44
|
+
description = stripped[:200]
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
# Register as a resource — capture content in closure
|
|
48
|
+
_register_one(mcp, uri, name, description, content)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _register_one(mcp, uri: str, name: str, description: str, content: str) -> None:
|
|
52
|
+
"""Register a single doc resource. Separate function to capture closure correctly."""
|
|
53
|
+
|
|
54
|
+
@mcp.resource(uri, name=name, description=description)
|
|
55
|
+
def _read() -> str:
|
|
56
|
+
return content
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Server helpers for Cooperage MCP servers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def serve(mcp, host: str = "0.0.0.0", port: int | None = None) -> None:
|
|
7
|
+
"""Start the MCP server. Replaces the uvicorn boilerplate.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
mcp: A FastMCP instance.
|
|
11
|
+
host: Bind address (default: 0.0.0.0).
|
|
12
|
+
port: Port to listen on (default: PORT env var or 8000).
|
|
13
|
+
"""
|
|
14
|
+
import uvicorn
|
|
15
|
+
|
|
16
|
+
if port is None:
|
|
17
|
+
port = int(os.environ.get("PORT", "8000"))
|
|
18
|
+
|
|
19
|
+
uvicorn.run(mcp.streamable_http_app(), host=host, port=port)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Workspace helpers for Cooperage MCP servers."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Workspace:
|
|
8
|
+
"""Interface for the shared /workspace volume.
|
|
9
|
+
|
|
10
|
+
Handles path resolution, traversal protection, and the
|
|
11
|
+
COOPERAGE_WORKSPACE env var so server devs don't have to.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._root = Path(os.environ.get("COOPERAGE_WORKSPACE", "/workspace"))
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def root(self) -> Path:
|
|
19
|
+
"""The workspace root as a Path."""
|
|
20
|
+
return self._root
|
|
21
|
+
|
|
22
|
+
def path(self, path: str) -> Path:
|
|
23
|
+
"""Resolve a relative path within the workspace.
|
|
24
|
+
|
|
25
|
+
Returns a full Path you can use with any library:
|
|
26
|
+
|
|
27
|
+
data = workspace.path("input.csv").read_text()
|
|
28
|
+
image = Image.open(workspace.path("photo.png"))
|
|
29
|
+
df = pd.read_csv(workspace.path("data.csv"))
|
|
30
|
+
plt.savefig(workspace.path("chart.png"))
|
|
31
|
+
|
|
32
|
+
Raises ValueError on path traversal attempts.
|
|
33
|
+
"""
|
|
34
|
+
full = (self._root / path).resolve()
|
|
35
|
+
if not str(full).startswith(str(self._root.resolve())):
|
|
36
|
+
raise ValueError(f"Path {path!r} escapes the workspace")
|
|
37
|
+
return full
|
|
38
|
+
|
|
39
|
+
def exists(self, path: str) -> bool:
|
|
40
|
+
"""Check if a file exists in the workspace."""
|
|
41
|
+
return self.path(path).exists()
|
|
42
|
+
|
|
43
|
+
def list(self, subdir: str = "") -> list[str]:
|
|
44
|
+
"""List files in the workspace (or a subdirectory), relative to root."""
|
|
45
|
+
target = self.path(subdir) if subdir else self._root
|
|
46
|
+
if not target.exists():
|
|
47
|
+
return []
|
|
48
|
+
return sorted(
|
|
49
|
+
str(p.relative_to(self._root))
|
|
50
|
+
for p in target.rglob("*")
|
|
51
|
+
if p.is_file()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
workspace = Workspace()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cooperage-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight helpers for writing Cooperage-compatible MCP servers"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"mcp[cli]>=1.0.0",
|
|
13
|
+
"uvicorn>=0.27.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["cooperage_sdk"]
|