ucpcore-server 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,8 @@
1
+ .venv
2
+ __pycache__
3
+ *.egg-info
4
+ .pytest_cache
5
+ dist
6
+ tests
7
+ Dockerfile
8
+ .dockerignore
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ package-lock.json
3
+ .venv/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ .DS_Store
@@ -0,0 +1,38 @@
1
+ # syntax=docker/dockerfile:1
2
+
3
+ # --- build stage: build the wheel, keep build tooling out of the image -------
4
+ FROM python:3.12-slim AS build
5
+
6
+ WORKDIR /build
7
+ COPY pyproject.toml README.md ./
8
+ COPY src ./src
9
+ RUN pip install --no-cache-dir build && python -m build --wheel --outdir /dist
10
+
11
+ # --- runtime stage ------------------------------------------------------------
12
+ FROM python:3.12-slim
13
+
14
+ LABEL org.opencontainers.image.title="ucp-server" \
15
+ org.opencontainers.image.description="Self-hosted UCP generation service: REST + MCP (Streamable HTTP)" \
16
+ org.opencontainers.image.url="https://ucpcore.org" \
17
+ org.opencontainers.image.source="https://github.com/ucpcore/ucp" \
18
+ org.opencontainers.image.licenses="Apache-2.0" \
19
+ org.opencontainers.image.vendor="UCP contributors"
20
+
21
+ RUN useradd --create-home --uid 1000 ucp
22
+
23
+ COPY --from=build /dist/*.whl /tmp/
24
+ RUN pip install --no-cache-dir /tmp/*.whl && rm /tmp/*.whl
25
+
26
+ # 0.0.0.0 is deliberate inside the container: the container boundary is the
27
+ # isolation; publish the port consciously (see README, Security).
28
+ ENV UCP_SERVER_HOST=0.0.0.0 \
29
+ UCP_SERVER_PORT=8080 \
30
+ UCP_CACHE_DIR=/home/ucp/.cache/ucp-server
31
+
32
+ USER ucp
33
+ EXPOSE 8080
34
+
35
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
36
+ CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=4)"]
37
+
38
+ ENTRYPOINT ["ucp-server"]
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: ucpcore-server
3
+ Version: 0.1.0
4
+ Summary: Self-hosted UCP server: generate Universal Context Packages from GitHub/Jira over REST and MCP (Streamable HTTP). One command to run.
5
+ Project-URL: Specification, https://github.com/ucpcore/ucp
6
+ Project-URL: Homepage, https://ucpcore.org
7
+ Author: UCP contributors
8
+ License-Expression: Apache-2.0
9
+ Keywords: context,context-engineering,llm,mcp,server,ucp
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: fastapi>=0.115
12
+ Requires-Dist: fastmcp>=2.3
13
+ Requires-Dist: httpx>=0.27
14
+ Requires-Dist: pydantic-settings>=2.4
15
+ Requires-Dist: pydantic>=2.7
16
+ Requires-Dist: pyucp>=0.1.0
17
+ Requires-Dist: ucp-gen>=0.3.0
18
+ Requires-Dist: uvicorn>=0.30
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
21
+ Requires-Dist: pytest>=8; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # ucp-server — self-hosted UCP generation service
25
+
26
+ One process, two interfaces. Point it at GitHub/Jira credentials and get
27
+ [Universal Context Packages](https://ucpcore.org) on demand:
28
+
29
+ - **REST API** (`/v1`) — generate and fetch packages with `curl` or any HTTP client;
30
+ - **MCP over Streamable HTTP** (`/mcp`) — plug it straight into Cursor,
31
+ Claude Code, or any MCP-capable agent.
32
+
33
+ Configuration is environment-only (12-factor), generated packages are cached
34
+ on disk with a TTL, and authentication is one env var away. Install it,
35
+ start it, forget it.
36
+
37
+ > PyPI package name: **`ucpcore-server`** (the name `ucp-server` was taken).
38
+ > The command it installs is still `ucp-server`.
39
+
40
+ ## Quickstart
41
+
42
+ ### Docker (one command)
43
+
44
+ ```bash
45
+ docker run --rm -p 8080:8080 -e GITHUB_TOKEN=ghp_yourtoken \
46
+ ghcr.io/ucpcore/ucp-server:latest
47
+ ```
48
+
49
+ ### uvx / pipx (no Docker)
50
+
51
+ ```bash
52
+ uvx --from ucpcore-server ucp-server
53
+ # or: pipx run --spec ucpcore-server ucp-server
54
+ ```
55
+
56
+ Then generate a package from any public GitHub issue:
57
+
58
+ ```bash
59
+ curl -s -X POST http://localhost:8080/v1/generate \
60
+ -H 'Content-Type: application/json' \
61
+ -d '{"source": "github", "ref": "pallets/flask#5961"}' | head
62
+ ```
63
+
64
+ Interactive API docs: <http://localhost:8080/docs>.
65
+
66
+ ## Connect an agent (MCP)
67
+
68
+ The MCP endpoint speaks Streamable HTTP at `http://localhost:8080/mcp`.
69
+
70
+ Cursor / Claude Code (`mcp.json`):
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "ucp": {
76
+ "url": "http://localhost:8080/mcp"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ If the server runs with `UCP_SERVER_API_KEY`, add the header:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "ucp": {
88
+ "url": "http://localhost:8080/mcp",
89
+ "headers": { "Authorization": "Bearer YOUR_KEY" }
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ Tools exposed:
96
+
97
+ | Tool | Purpose |
98
+ |---|---|
99
+ | `generate_context(source, ref, llm=False)` | Build a UCP from a GitHub issue (`owner/repo#123`) or Jira ticket (`PROJ-123`) |
100
+ | `list_contexts()` | List cached packages: id, entity, title, freshness |
101
+ | `get_context(id)` | Full UCP JSON for a cached package |
102
+ | `get_context_markdown(id, token_budget?)` | Canonical Markdown rendering (SPEC §7), optionally truncated by salience |
103
+
104
+ ## REST API
105
+
106
+ | Method & path | Purpose |
107
+ |---|---|
108
+ | `POST /v1/generate` | Generate a package. Body: `{"source": "github"\|"jira", "ref": "...", "llm": false, "since": null, "audience": null}`. Returns the UCP JSON; headers `X-UCP-Package-Id` and `X-UCP-Cache: hit\|miss`. |
109
+ | `GET /v1/packages` | Cached packages: id, title, entity, generated_at |
110
+ | `GET /v1/packages/{id}` | Full UCP JSON |
111
+ | `GET /v1/packages/{id}/markdown?token_budget=1500` | Canonical Markdown rendering |
112
+ | `GET /healthz`, `GET /readyz` | Liveness / readiness probes (never authenticated) |
113
+ | `GET /docs`, `GET /openapi.json` | OpenAPI documentation |
114
+
115
+ Errors are RFC 9457 problem documents (`application/problem+json`):
116
+
117
+ ```json
118
+ {"type": "https://ucpcore.org/problems/invalid-ref", "title": "Invalid Reference",
119
+ "status": 400, "detail": "invalid GitHub reference 'x': expected owner/repo#number..."}
120
+ ```
121
+
122
+ Examples:
123
+
124
+ ```bash
125
+ # Jira (needs JIRA_* env on the server)
126
+ curl -s -X POST http://localhost:8080/v1/generate \
127
+ -H 'Content-Type: application/json' \
128
+ -d '{"source": "jira", "ref": "PROJ-123"}'
129
+
130
+ # LLM-enhanced (needs UCP_LLM_* env on the server)
131
+ curl -s -X POST http://localhost:8080/v1/generate \
132
+ -H 'Content-Type: application/json' \
133
+ -d '{"source": "github", "ref": "microsoft/vscode#519", "llm": true}'
134
+
135
+ # Rendered Markdown under a token budget
136
+ curl -s "http://localhost:8080/v1/packages/github-pallets-flask-5961/markdown?token_budget=1500"
137
+ ```
138
+
139
+ ## Configuration
140
+
141
+ Everything is optional; the server starts with zero configuration and
142
+ reports clearly when a credential is missing for a requested source.
143
+
144
+ | Variable | Default | Purpose |
145
+ |---|---|---|
146
+ | `UCP_SERVER_HOST` | `127.0.0.1` (Docker image: `0.0.0.0`) | Bind address |
147
+ | `UCP_SERVER_PORT` | `8080` | Bind port |
148
+ | `UCP_SERVER_API_KEY` | *(unset — auth disabled)* | When set, all endpoints except health probes require `Authorization: Bearer <key>` |
149
+ | `UCP_CACHE_DIR` | `~/.cache/ucp-server` | Disk cache for generated packages |
150
+ | `UCP_CACHE_TTL` | `900` (15 min) | Cache TTL in seconds; `0` disables caching |
151
+ | `GITHUB_TOKEN` / `GH_TOKEN` | *(unset)* | GitHub token (public issues work without it, at a low rate limit) |
152
+ | `JIRA_BASE_URL` | *(unset)* | e.g. `https://yourcompany.atlassian.net` |
153
+ | `JIRA_EMAIL` | *(unset)* | Jira Cloud email (Basic auth); omit for Server/DC PAT |
154
+ | `JIRA_API_TOKEN` | *(unset)* | Jira API token or PAT |
155
+ | `UCP_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint for `llm: true` |
156
+ | `UCP_LLM_API_KEY` | *(unset)* | LLM API key (falls back to `OPENAI_API_KEY`) |
157
+ | `UCP_LLM_MODEL` | `gpt-4o-mini` | LLM model name |
158
+ | `UCP_LOG_JSON` | `false` | `1`/`true` switches to JSON-lines logs |
159
+ | `UCP_LOG_LEVEL` | `INFO` | Log level |
160
+
161
+ ## Security
162
+
163
+ - **Set `UCP_SERVER_API_KEY` for any non-localhost deployment.** Without it
164
+ anyone who can reach the port can spend your GitHub/Jira/LLM quota. The
165
+ key is compared in constant time; health probes stay open for orchestrators.
166
+ - **Bind is `127.0.0.1` by default** when run directly. The Docker image
167
+ sets `UCP_SERVER_HOST=0.0.0.0` deliberately — the container boundary is
168
+ the isolation there; publish the port consciously (`-p 127.0.0.1:8080:8080`
169
+ keeps it local).
170
+ - **No client-supplied URLs.** Clients pass references (`owner/repo#123`,
171
+ `PROJ-123`) to the two predefined connectors; the server never fetches an
172
+ arbitrary URL on a client's behalf (no SSRF surface).
173
+ - Request bodies are limited to 64 KiB and validated strictly (unknown
174
+ fields rejected). Tokens are masked in logs. The Docker image runs as a
175
+ non-root user.
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ python3 -m venv .venv && . .venv/bin/activate
181
+ pip install -e ".[dev]"
182
+ pytest
183
+ ```
184
+
185
+ Docker image:
186
+
187
+ ```bash
188
+ docker build -t ucp-server .
189
+ docker run --rm -p 8080:8080 ucp-server
190
+ ```
@@ -0,0 +1,167 @@
1
+ # ucp-server — self-hosted UCP generation service
2
+
3
+ One process, two interfaces. Point it at GitHub/Jira credentials and get
4
+ [Universal Context Packages](https://ucpcore.org) on demand:
5
+
6
+ - **REST API** (`/v1`) — generate and fetch packages with `curl` or any HTTP client;
7
+ - **MCP over Streamable HTTP** (`/mcp`) — plug it straight into Cursor,
8
+ Claude Code, or any MCP-capable agent.
9
+
10
+ Configuration is environment-only (12-factor), generated packages are cached
11
+ on disk with a TTL, and authentication is one env var away. Install it,
12
+ start it, forget it.
13
+
14
+ > PyPI package name: **`ucpcore-server`** (the name `ucp-server` was taken).
15
+ > The command it installs is still `ucp-server`.
16
+
17
+ ## Quickstart
18
+
19
+ ### Docker (one command)
20
+
21
+ ```bash
22
+ docker run --rm -p 8080:8080 -e GITHUB_TOKEN=ghp_yourtoken \
23
+ ghcr.io/ucpcore/ucp-server:latest
24
+ ```
25
+
26
+ ### uvx / pipx (no Docker)
27
+
28
+ ```bash
29
+ uvx --from ucpcore-server ucp-server
30
+ # or: pipx run --spec ucpcore-server ucp-server
31
+ ```
32
+
33
+ Then generate a package from any public GitHub issue:
34
+
35
+ ```bash
36
+ curl -s -X POST http://localhost:8080/v1/generate \
37
+ -H 'Content-Type: application/json' \
38
+ -d '{"source": "github", "ref": "pallets/flask#5961"}' | head
39
+ ```
40
+
41
+ Interactive API docs: <http://localhost:8080/docs>.
42
+
43
+ ## Connect an agent (MCP)
44
+
45
+ The MCP endpoint speaks Streamable HTTP at `http://localhost:8080/mcp`.
46
+
47
+ Cursor / Claude Code (`mcp.json`):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "ucp": {
53
+ "url": "http://localhost:8080/mcp"
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ If the server runs with `UCP_SERVER_API_KEY`, add the header:
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "ucp": {
65
+ "url": "http://localhost:8080/mcp",
66
+ "headers": { "Authorization": "Bearer YOUR_KEY" }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ Tools exposed:
73
+
74
+ | Tool | Purpose |
75
+ |---|---|
76
+ | `generate_context(source, ref, llm=False)` | Build a UCP from a GitHub issue (`owner/repo#123`) or Jira ticket (`PROJ-123`) |
77
+ | `list_contexts()` | List cached packages: id, entity, title, freshness |
78
+ | `get_context(id)` | Full UCP JSON for a cached package |
79
+ | `get_context_markdown(id, token_budget?)` | Canonical Markdown rendering (SPEC §7), optionally truncated by salience |
80
+
81
+ ## REST API
82
+
83
+ | Method & path | Purpose |
84
+ |---|---|
85
+ | `POST /v1/generate` | Generate a package. Body: `{"source": "github"\|"jira", "ref": "...", "llm": false, "since": null, "audience": null}`. Returns the UCP JSON; headers `X-UCP-Package-Id` and `X-UCP-Cache: hit\|miss`. |
86
+ | `GET /v1/packages` | Cached packages: id, title, entity, generated_at |
87
+ | `GET /v1/packages/{id}` | Full UCP JSON |
88
+ | `GET /v1/packages/{id}/markdown?token_budget=1500` | Canonical Markdown rendering |
89
+ | `GET /healthz`, `GET /readyz` | Liveness / readiness probes (never authenticated) |
90
+ | `GET /docs`, `GET /openapi.json` | OpenAPI documentation |
91
+
92
+ Errors are RFC 9457 problem documents (`application/problem+json`):
93
+
94
+ ```json
95
+ {"type": "https://ucpcore.org/problems/invalid-ref", "title": "Invalid Reference",
96
+ "status": 400, "detail": "invalid GitHub reference 'x': expected owner/repo#number..."}
97
+ ```
98
+
99
+ Examples:
100
+
101
+ ```bash
102
+ # Jira (needs JIRA_* env on the server)
103
+ curl -s -X POST http://localhost:8080/v1/generate \
104
+ -H 'Content-Type: application/json' \
105
+ -d '{"source": "jira", "ref": "PROJ-123"}'
106
+
107
+ # LLM-enhanced (needs UCP_LLM_* env on the server)
108
+ curl -s -X POST http://localhost:8080/v1/generate \
109
+ -H 'Content-Type: application/json' \
110
+ -d '{"source": "github", "ref": "microsoft/vscode#519", "llm": true}'
111
+
112
+ # Rendered Markdown under a token budget
113
+ curl -s "http://localhost:8080/v1/packages/github-pallets-flask-5961/markdown?token_budget=1500"
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ Everything is optional; the server starts with zero configuration and
119
+ reports clearly when a credential is missing for a requested source.
120
+
121
+ | Variable | Default | Purpose |
122
+ |---|---|---|
123
+ | `UCP_SERVER_HOST` | `127.0.0.1` (Docker image: `0.0.0.0`) | Bind address |
124
+ | `UCP_SERVER_PORT` | `8080` | Bind port |
125
+ | `UCP_SERVER_API_KEY` | *(unset — auth disabled)* | When set, all endpoints except health probes require `Authorization: Bearer <key>` |
126
+ | `UCP_CACHE_DIR` | `~/.cache/ucp-server` | Disk cache for generated packages |
127
+ | `UCP_CACHE_TTL` | `900` (15 min) | Cache TTL in seconds; `0` disables caching |
128
+ | `GITHUB_TOKEN` / `GH_TOKEN` | *(unset)* | GitHub token (public issues work without it, at a low rate limit) |
129
+ | `JIRA_BASE_URL` | *(unset)* | e.g. `https://yourcompany.atlassian.net` |
130
+ | `JIRA_EMAIL` | *(unset)* | Jira Cloud email (Basic auth); omit for Server/DC PAT |
131
+ | `JIRA_API_TOKEN` | *(unset)* | Jira API token or PAT |
132
+ | `UCP_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint for `llm: true` |
133
+ | `UCP_LLM_API_KEY` | *(unset)* | LLM API key (falls back to `OPENAI_API_KEY`) |
134
+ | `UCP_LLM_MODEL` | `gpt-4o-mini` | LLM model name |
135
+ | `UCP_LOG_JSON` | `false` | `1`/`true` switches to JSON-lines logs |
136
+ | `UCP_LOG_LEVEL` | `INFO` | Log level |
137
+
138
+ ## Security
139
+
140
+ - **Set `UCP_SERVER_API_KEY` for any non-localhost deployment.** Without it
141
+ anyone who can reach the port can spend your GitHub/Jira/LLM quota. The
142
+ key is compared in constant time; health probes stay open for orchestrators.
143
+ - **Bind is `127.0.0.1` by default** when run directly. The Docker image
144
+ sets `UCP_SERVER_HOST=0.0.0.0` deliberately — the container boundary is
145
+ the isolation there; publish the port consciously (`-p 127.0.0.1:8080:8080`
146
+ keeps it local).
147
+ - **No client-supplied URLs.** Clients pass references (`owner/repo#123`,
148
+ `PROJ-123`) to the two predefined connectors; the server never fetches an
149
+ arbitrary URL on a client's behalf (no SSRF surface).
150
+ - Request bodies are limited to 64 KiB and validated strictly (unknown
151
+ fields rejected). Tokens are masked in logs. The Docker image runs as a
152
+ non-root user.
153
+
154
+ ## Development
155
+
156
+ ```bash
157
+ python3 -m venv .venv && . .venv/bin/activate
158
+ pip install -e ".[dev]"
159
+ pytest
160
+ ```
161
+
162
+ Docker image:
163
+
164
+ ```bash
165
+ docker build -t ucp-server .
166
+ docker run --rm -p 8080:8080 ucp-server
167
+ ```
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ucpcore-server"
7
+ version = "0.1.0"
8
+ description = "Self-hosted UCP server: generate Universal Context Packages from GitHub/Jira over REST and MCP (Streamable HTTP). One command to run."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "UCP contributors" }]
13
+ keywords = ["ucp", "mcp", "llm", "context", "context-engineering", "server"]
14
+ dependencies = [
15
+ "pyucp>=0.1.0",
16
+ "ucp-gen>=0.3.0",
17
+ "fastapi>=0.115",
18
+ "uvicorn>=0.30",
19
+ "fastmcp>=2.3",
20
+ "pydantic>=2.7",
21
+ "pydantic-settings>=2.4",
22
+ "httpx>=0.27",
23
+ ]
24
+
25
+ [project.urls]
26
+ Specification = "https://github.com/ucpcore/ucp"
27
+ Homepage = "https://ucpcore.org"
28
+
29
+ [project.scripts]
30
+ ucp-server = "ucp_server.__main__:main"
31
+ ucpcore-server = "ucp_server.__main__:main"
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=8", "pytest-asyncio>=0.23"]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/ucp_server"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ """ucp-server — self-hosted UCP generation service (REST + MCP)."""
2
+ __version__ = "0.1.0"
3
+
4
+ __all__ = ["__version__"]
@@ -0,0 +1,52 @@
1
+ """Entry point: ``ucp-server`` (or ``python -m ucp_server``).
2
+
3
+ All configuration comes from the environment; see README for the full table.
4
+ Uvicorn handles SIGINT/SIGTERM for a graceful shutdown.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import sys
10
+
11
+ from .config import ConfigError, load_settings
12
+ from .logging_setup import configure_logging
13
+
14
+
15
+ def main() -> int:
16
+ try:
17
+ settings = load_settings()
18
+ except ConfigError as exc:
19
+ print(f"ucp-server: {exc}", file=sys.stderr)
20
+ return 2
21
+
22
+ configure_logging(json_logs=settings.log_json, level=settings.log_level)
23
+ logger = logging.getLogger("ucp_server")
24
+ logger.info("ucp-server starting on %s:%s", settings.host, settings.port)
25
+ if settings.api_key:
26
+ logger.info("authentication: enabled (Bearer)")
27
+ else:
28
+ logger.warning(
29
+ "authentication: DISABLED — set UCP_SERVER_API_KEY before exposing "
30
+ "this server beyond localhost"
31
+ )
32
+ if settings.cache_ttl > 0:
33
+ logger.info("cache: %s (ttl %ss)", settings.cache_dir, settings.cache_ttl)
34
+ else:
35
+ logger.info("cache: disabled (UCP_CACHE_TTL=0)")
36
+
37
+ import uvicorn
38
+
39
+ from .app import create_app
40
+
41
+ uvicorn.run(
42
+ create_app(settings),
43
+ host=settings.host,
44
+ port=settings.port,
45
+ log_config=None, # keep our logging configuration
46
+ timeout_graceful_shutdown=10,
47
+ )
48
+ return 0
49
+
50
+
51
+ if __name__ == "__main__":
52
+ raise SystemExit(main())