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.
- ucpcore_server-0.1.0/.dockerignore +8 -0
- ucpcore_server-0.1.0/.gitignore +6 -0
- ucpcore_server-0.1.0/Dockerfile +38 -0
- ucpcore_server-0.1.0/PKG-INFO +190 -0
- ucpcore_server-0.1.0/README.md +167 -0
- ucpcore_server-0.1.0/pyproject.toml +41 -0
- ucpcore_server-0.1.0/src/ucp_server/__init__.py +4 -0
- ucpcore_server-0.1.0/src/ucp_server/__main__.py +52 -0
- ucpcore_server-0.1.0/src/ucp_server/app.py +203 -0
- ucpcore_server-0.1.0/src/ucp_server/cache.py +86 -0
- ucpcore_server-0.1.0/src/ucp_server/config.py +76 -0
- ucpcore_server-0.1.0/src/ucp_server/logging_setup.py +54 -0
- ucpcore_server-0.1.0/src/ucp_server/mcp_tools.py +94 -0
- ucpcore_server-0.1.0/src/ucp_server/service.py +123 -0
- ucpcore_server-0.1.0/tests/__init__.py +0 -0
- ucpcore_server-0.1.0/tests/conftest.py +49 -0
- ucpcore_server-0.1.0/tests/fixtures.py +55 -0
- ucpcore_server-0.1.0/tests/test_api.py +138 -0
- ucpcore_server-0.1.0/tests/test_auth.py +65 -0
- ucpcore_server-0.1.0/tests/test_cache.py +67 -0
- ucpcore_server-0.1.0/tests/test_config.py +55 -0
- ucpcore_server-0.1.0/tests/test_logging.py +17 -0
- ucpcore_server-0.1.0/tests/test_mcp.py +71 -0
|
@@ -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,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())
|