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.
@@ -0,0 +1 @@
1
+ __pycache__/
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: cooperage-sdk
3
+ Version: 0.1.0
4
+ Summary: Lightweight helpers for writing Cooperage-compatible MCP servers
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: mcp[cli]>=1.0.0
8
+ Requires-Dist: uvicorn>=0.27.0
@@ -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"]