mirror of
https://github.com/go-sigma/sigma.git
synced 2026-10-28 13:34:34 +08:00
refactor(server)!: remove the embedded MCP server
Sigma shipped three overlapping ways to operate a remote server: the embedded MCP server, sigma-cli, and the sigma-api-operator skill. All three wrapped the same HTTP API, yet each covered a different subset: user CRUD existed only in the CLI, system-* only in MCP, and builders, daemons, webhooks, validators, code repositories and tokens were reachable only through raw HTTP. MCP was disabled by default and still carried 1246 lines of production code plus 606 lines of tests to maintain that partially overlapping surface. Keep two surfaces instead: sigma-cli for execution and the skill for API discovery. - delete pkg/server/mcp and its mcp-go dependency, which also drops the jsonschema-go, jsonschema/v6 and uritemplate/v3 transitives - drop ConfigurationMCP, its defaults and the authz/audit skipper bypasses that existed only to let the MCP endpoint bypass both - remove the mcp section from conf/config*.yaml, docs/configuration.md, and the Helm chart values and configmap - replace docs/mcp.md with docs/cli.md covering sigma-cli login, global flags, per-resource commands, jq composition and agent usage - make sigma-cli the preferred path in the sigma-api-operator skill and rebuild its examples around the resources the CLI does not wrap yet, using the paths and request schemas from swagger.yaml Lint was already red on dev for an unrelated reason: golangci-lint v2.13.0 vendors golang.org/x/tools v0.49.0, which only decodes export data version 4, so every import of a Go 1.27.2 standard library package failed typecheck with "export data version 5 is greater than maximum supported version 4". Bump to v2.15.0 (x/tools v0.52.0) and fix the two modernize findings it reports for Go 1.27's strings.CutLast. - .github/workflows/lint.yml: golangci-lint v2.13.0 -> v2.15.0, and document the export data constraint next to the pin - pkg/authz/resolver.go, pkg/logger/logger.go: replace LastIndex slicing with strings.CutLast - bump github.com/docker/cli v29.9.0 -> v29.9.1 BREAKING CHANGE: the `mcp` configuration section and the /api/v1/mcp endpoint are gone. Use sigma-cli or the REST API for automation. Verification: - go build -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./... - go vet -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./... - golangci-lint v2.15.0 run --build-tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./... (0 issues) - golangci-lint config verify - go test -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./pkg/authz/... ./pkg/logger/... ./cmd/cli/... ./pkg/app/... ./pkg/background/build/... - go test -run TestConfiguration -tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp" ./pkg/config/... - go mod tidy - make swagen - helm template sigma ./deploy/sigma - bun run docs:build - make build - make build-cli
This commit is contained in:
1 parent
940ab15dd4
commit
187f5c4d97
45 files changed
+418
-2352
No files matched your search
@@ -53,7 +53,10 @@ jobs:
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.13.0
|
||||
# Pin a release whose vendored golang.org/x/tools can decode the
|
||||
# toolchain's export data. Older releases fail typecheck with
|
||||
# "export data version N is greater than maximum supported version M".
|
||||
version: v2.15.0
|
||||
args: --timeout=30m --verbose --build-tags "netgo,timetzdata,exclude_graphdriver_btrfs,containers_image_openpgp"
|
||||
skip-cache: false
|
||||
- name: Lint Dockerfile
|
||||
|
||||
@@ -312,7 +312,7 @@ Directory rules:
|
||||
|
||||
## CI Workflows (`.github/workflows/`)
|
||||
|
||||
- **lint.yml**: Go 1.27 + Bun web build + golangci-lint v2.13.0 + hadolint on `build/Dockerfile`. Triggers on `main`/`dev` PRs.
|
||||
- **lint.yml**: Go 1.27 + Bun web build + golangci-lint v2.15.0 + hadolint on `build/Dockerfile`. Triggers on `main`/`dev` PRs.
|
||||
- **test.yml**: Go 1.27 + Bun web build; runs application tests on PostgreSQL and database tests on `sqlite3`, `turso`, `postgresql`, and `mysql`. Uses MySQL 8.0, Postgres 15, Redis 7, and rs3 services. Uploads coverage to Coveralls.
|
||||
- **govulncheck.yml**: runs the pinned `go tool govulncheck` (with the project build tags) to scan for known reachable vulnerabilities, writing the report to the job summary. Non-blocking (`continue-on-error`): findings are reported but do not fail the workflow. Triggers on `dev` push/PR.
|
||||
- **e2e.yml**, **image-build.yml**, **gh-pages.yml**: end-to-end, image build, docs deploy.
|
||||
|
||||
@@ -49,12 +49,13 @@ The diagram below maps Sigma's main runtime layers, from client entry points to
|
||||
|
||||
Sigma provides two integration paths for AI clients and external automation:
|
||||
|
||||
- Embedded MCP server: Sigma can expose registry management tools through the
|
||||
existing server process. It is disabled by default and can be enabled through
|
||||
the `mcp` configuration section. See [MCP Server](docs/mcp.md).
|
||||
- `sigma-cli`: a single binary that operates a remote Sigma server over the REST
|
||||
API. It covers namespaces, repositories, tags, artifacts, members, and users.
|
||||
See [CLI & AI Agents](docs/cli.md).
|
||||
- External REST API skill: `tools/skill/sigma-api-operator` packages a reusable
|
||||
skill and `swagger.yaml` so CLI agents such as Claude Code or Copilot CLI can
|
||||
discover Sigma APIs and operate resources through HTTP requests.
|
||||
discover Sigma APIs and operate resources through HTTP requests, including the
|
||||
areas the CLI does not wrap yet.
|
||||
|
||||
## Alternatives
|
||||
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@ docker run --name sigma -p 3000:3000 --rm tosone/sigma:nightly-alpine
|
||||
|
||||
Sigma 为 AI 客户端和外部自动化工具提供了两种集成方式:
|
||||
|
||||
- 内置 MCP server:Sigma 可以通过现有的 server 进程暴露镜像仓库管理工具。该能力默认关闭,可以通过 `mcp` 配置项启用。详细说明见 [MCP Server](docs/mcp.md)。
|
||||
- 外部 REST API skill:`tools/skill/sigma-api-operator` 打包了可复用的 skill 和 `swagger.yaml`,方便 Claude Code、Copilot CLI 等 CLI agent 发现 Sigma API,并通过 HTTP 请求操作资源。
|
||||
- `sigma-cli`:单个二进制,通过 REST API 操作远程 Sigma 服务端,覆盖命名空间、仓库、Tag、制品、成员和用户。详细说明见 [CLI 与 AI Agent](docs/cli.md)。
|
||||
- 外部 REST API skill:`tools/skill/sigma-api-operator` 打包了可复用的 skill 和 `swagger.yaml`,方便 Claude Code、Copilot CLI 等 CLI agent 发现 Sigma API,并通过 HTTP 请求操作资源,包括 CLI 尚未封装的接口。
|
||||
|
||||
## Alternatives
|
||||
|
||||
|
||||
@@ -177,14 +177,6 @@ auth:
|
||||
window: 5m
|
||||
delay: 1s
|
||||
|
||||
mcp:
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
|
||||
audit:
|
||||
enabled: true
|
||||
cleanup:
|
||||
|
||||
@@ -192,14 +192,6 @@ auth:
|
||||
window: 5m
|
||||
delay: 1s
|
||||
|
||||
mcp:
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
|
||||
audit:
|
||||
enabled: true
|
||||
recordListGet: false
|
||||
|
||||
@@ -201,14 +201,6 @@ auth:
|
||||
# fixed delay applied to every authentication attempt after maxFailures
|
||||
delay: 1s
|
||||
|
||||
mcp:
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
|
||||
audit:
|
||||
# record completed authenticated business HTTP requests
|
||||
enabled: true
|
||||
|
||||
@@ -221,15 +221,6 @@ auth:
|
||||
# inmemory | redis
|
||||
backend: inmemory
|
||||
|
||||
mcp:
|
||||
# expose MCP management tools on the existing server port
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
|
||||
audit:
|
||||
# record completed authenticated business HTTP requests
|
||||
enabled: true
|
||||
|
||||
@@ -127,13 +127,6 @@ data:
|
||||
hardDeleteAfterMonths: {{ .Values.config.audit.cleanup.hardDeleteAfterMonths }}
|
||||
interval: {{ .Values.config.audit.cleanup.interval }}
|
||||
batchSize: {{ .Values.config.audit.cleanup.batchSize }}
|
||||
mcp:
|
||||
enabled: {{ .Values.config.mcp.enabled }}
|
||||
path: {{ .Values.config.mcp.path | quote }}
|
||||
transport: {{ .Values.config.mcp.transport | quote }}
|
||||
auditWrites: {{ .Values.config.mcp.auditWrites }}
|
||||
toolTimeout: {{ .Values.config.mcp.toolTimeout }}
|
||||
maxRequestBody: {{ .Values.config.mcp.maxRequestBody | int64 }}
|
||||
auth:
|
||||
anonymous:
|
||||
enabled: true
|
||||
|
||||
@@ -214,13 +214,6 @@ config:
|
||||
hardDeleteAfterMonths: 6
|
||||
interval: 168h
|
||||
batchSize: 1000
|
||||
mcp:
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
auth:
|
||||
user:
|
||||
password: ""
|
||||
|
||||
@@ -6,7 +6,7 @@ const guideSidebar = [
|
||||
items: [
|
||||
{ text: 'Quick Start', link: '/quickstart' },
|
||||
{ text: 'Configuration', link: '/configuration' },
|
||||
{ text: 'MCP Server', link: '/mcp' },
|
||||
{ text: 'CLI & AI Agents', link: '/cli' },
|
||||
{ text: 'Cosign', link: '/cosign' }
|
||||
]
|
||||
},
|
||||
@@ -26,7 +26,7 @@ const zhGuideSidebar = [
|
||||
items: [
|
||||
{ text: '快速开始', link: '/zh/quickstart' },
|
||||
{ text: '配置', link: '/zh/configuration' },
|
||||
{ text: 'MCP Server', link: '/zh/mcp' },
|
||||
{ text: 'CLI 与 AI Agent', link: '/zh/cli' },
|
||||
{ text: 'Cosign', link: '/zh/cosign' }
|
||||
]
|
||||
},
|
||||
@@ -81,7 +81,7 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Docs', link: '/quickstart' },
|
||||
{ text: 'MCP', link: '/mcp' }
|
||||
{ text: 'CLI', link: '/cli' }
|
||||
],
|
||||
sidebar: guideSidebar,
|
||||
socialLinks: [
|
||||
@@ -105,7 +105,7 @@ export default defineConfig({
|
||||
nav: [
|
||||
{ text: '首页', link: '/zh/' },
|
||||
{ text: '文档', link: '/zh/quickstart' },
|
||||
{ text: 'MCP', link: '/zh/mcp' }
|
||||
{ text: 'CLI', link: '/zh/cli' }
|
||||
],
|
||||
sidebar: zhGuideSidebar,
|
||||
outline: {
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: CLI & AI Agents
|
||||
---
|
||||
|
||||
# CLI & AI Agents
|
||||
|
||||
Sigma exposes two ways to operate a remote server from a terminal or an AI
|
||||
agent. Both talk to the same HTTP API, so anything you can do in the Sigma web
|
||||
UI can be scripted.
|
||||
|
||||
| Path | Best for |
|
||||
| --- | --- |
|
||||
| `sigma-cli` | Humans, CI pipelines, and shell-capable agents |
|
||||
| `sigma-api-operator` skill | AI agents that need to discover APIs beyond what the CLI covers |
|
||||
|
||||
There is no embedded MCP server. Agents that cannot execute commands should use
|
||||
a thin HTTP client against the REST API described by the OpenAPI document.
|
||||
|
||||
## Build the CLI
|
||||
|
||||
```bash
|
||||
make build-cli
|
||||
./bin/sigma-cli --help
|
||||
```
|
||||
|
||||
## Login
|
||||
|
||||
`sigma-cli` stores Basic Auth credentials in `~/.sigma/cli-credentials.json`
|
||||
with `0600` permissions. Override the location with `--credentials-file` or
|
||||
`SIGMA_CREDENTIALS_FILE`.
|
||||
|
||||
```bash
|
||||
./bin/sigma-cli login \
|
||||
--server https://sigma.example.com \
|
||||
--username sigma \
|
||||
--password 'Admin@123'
|
||||
```
|
||||
|
||||
Credentials can also be supplied without a credentials file through the
|
||||
persistent flags or environment variables:
|
||||
|
||||
```bash
|
||||
export SIGMA_SERVER=https://sigma.example.com
|
||||
export SIGMA_USERNAME=sigma
|
||||
export SIGMA_PASSWORD='Admin@123'
|
||||
```
|
||||
|
||||
Global flags:
|
||||
|
||||
| Flag | Env | Default |
|
||||
| --- | --- | --- |
|
||||
| `--server` | `SIGMA_SERVER` | `http://localhost:3000` |
|
||||
| `--credentials-file` | `SIGMA_CREDENTIALS_FILE` | `~/.sigma/cli-credentials.json` |
|
||||
| `--timeout` | – | `30s` |
|
||||
| `--debug` | – | `false` |
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Namespace
|
||||
|
||||
```bash
|
||||
sigma-cli namespace list --page 1 --limit 10
|
||||
sigma-cli namespace get <namespace_id>
|
||||
sigma-cli namespace create library --visibility public --description "Base images"
|
||||
sigma-cli namespace update <namespace_id> --size-limit 107374182400
|
||||
sigma-cli namespace delete <namespace_id>
|
||||
```
|
||||
|
||||
### Repository
|
||||
|
||||
```bash
|
||||
sigma-cli repo list <namespace_id>
|
||||
sigma-cli repo get <namespace_id> <repository_id>
|
||||
sigma-cli repo create <namespace_id> library/alpine --visibility public
|
||||
sigma-cli repo update <namespace_id> <repository_id> --description "Alpine images"
|
||||
sigma-cli repo delete <namespace_id> <repository_id>
|
||||
```
|
||||
|
||||
### Tag
|
||||
|
||||
```bash
|
||||
sigma-cli tag list <namespace_id> <repository_id> --type image
|
||||
sigma-cli tag get <namespace_id> <repository_id> <tag_id>
|
||||
sigma-cli tag delete <namespace_id> <repository_id> <tag_id>
|
||||
```
|
||||
|
||||
### Artifact
|
||||
|
||||
Artifact commands take namespace and repository **names** rather than IDs.
|
||||
|
||||
```bash
|
||||
sigma-cli artifact list <namespace_name> <repository_name>
|
||||
sigma-cli artifact get <namespace_name> <repository_name> <digest>
|
||||
sigma-cli artifact delete <namespace_name> <repository_name> <digest>
|
||||
```
|
||||
|
||||
### Member
|
||||
|
||||
```bash
|
||||
sigma-cli member list <namespace_id>
|
||||
sigma-cli member add <namespace_id> <user_id> --role reader
|
||||
sigma-cli member update <namespace_id> <user_id> --role admin
|
||||
sigma-cli member delete <namespace_id> <user_id>
|
||||
```
|
||||
|
||||
### User
|
||||
|
||||
```bash
|
||||
sigma-cli user list --without-admin
|
||||
sigma-cli user create --username alice --password '...' --email alice@example.com --role user
|
||||
sigma-cli user update <user_id> --role admin --status enabled
|
||||
```
|
||||
|
||||
All commands print JSON on success and a formatted error body on failure, so
|
||||
output composes with `jq`:
|
||||
|
||||
```bash
|
||||
sigma-cli namespace list --limit 100 | jq -r '.items[].name'
|
||||
```
|
||||
|
||||
## AI Agents
|
||||
|
||||
`tools/skill/sigma-api-operator` packages a skill for CLI agents such as Claude
|
||||
Code or Copilot CLI:
|
||||
|
||||
```text
|
||||
tools/skill/sigma-api-operator/SKILL.md
|
||||
tools/skill/sigma-api-operator/swagger.yaml
|
||||
```
|
||||
|
||||
The skill instructs the agent to load the OpenAPI document before issuing
|
||||
requests. This is required for resources the CLI does not wrap yet — builders,
|
||||
daemons, webhooks, validators, code repositories, and tokens — which must be
|
||||
called through the REST API.
|
||||
|
||||
Refresh the packaged specification after changing handlers:
|
||||
|
||||
```bash
|
||||
make swagen
|
||||
```
|
||||
|
||||
## Safety
|
||||
|
||||
- Destructive commands (`delete`, credential and privilege changes) require
|
||||
explicit confirmation by the agent.
|
||||
- Prefer HTTPS for remote servers; Basic Auth credentials travel with every
|
||||
request.
|
||||
- Do not print passwords, tokens, private keys, or `Authorization` headers.
|
||||
- Redact sensitive fields before surfacing non-2xx response bodies.
|
||||
@@ -206,14 +206,6 @@ auth:
|
||||
# fixed delay applied to every authentication attempt after maxFailures
|
||||
delay: 1s
|
||||
|
||||
mcp:
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
|
||||
audit:
|
||||
# record completed authenticated business HTTP requests
|
||||
enabled: true
|
||||
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
---
|
||||
title: MCP Server
|
||||
---
|
||||
|
||||
Sigma can expose an embedded MCP server for AI clients and automation tools.
|
||||
The MCP endpoint runs inside the existing `sigma server` process and is disabled by default.
|
||||
|
||||
## What It Exposes
|
||||
|
||||
The MCP server focuses on registry management metadata and controlled operations.
|
||||
It does not expose OCI blob upload/download traffic.
|
||||
|
||||
Current tools include:
|
||||
|
||||
- System: `system-version-get`, `system-endpoint-get`, `system-config-summary-get`
|
||||
- Namespace: `namespace-list`, `namespace-get`, `namespace-create`, `namespace-update`, `namespace-delete`
|
||||
- Namespace members: `namespace-member-list`, `namespace-member-add`, `namespace-member-update`, `namespace-member-delete`, `namespace-member-self-get`
|
||||
- Repository: `repository-list`, `repository-get`, `repository-create`, `repository-update`, `repository-delete`
|
||||
- Tag: `tag-list`, `tag-get`, `tag-delete`, `tag-manifest-raw-get`
|
||||
- Artifact: `artifact-list`, `artifact-get`, `artifact-delete`
|
||||
|
||||
The read-only resources include:
|
||||
|
||||
- `sigma://system/version`
|
||||
- `sigma://system/capabilities`
|
||||
|
||||
## Enable MCP
|
||||
|
||||
MCP is configured through the `mcp` section:
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
enabled: true
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
```
|
||||
|
||||
When deploying with Helm:
|
||||
|
||||
```bash
|
||||
helm upgrade --install sigma ./deploy/sigma \
|
||||
--set config.mcp.enabled=true
|
||||
```
|
||||
|
||||
The endpoint is served by the existing server service:
|
||||
|
||||
```text
|
||||
https://<sigma-host>/api/v1/mcp
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
MCP uses Sigma Basic Auth. Every MCP request must include:
|
||||
|
||||
```text
|
||||
Authorization: Basic <base64(username:password)>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
printf 'sigma:Admin@123' | base64
|
||||
```
|
||||
|
||||
Use the encoded value in the MCP client configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sigma": {
|
||||
"url": "https://<sigma-host>/api/v1/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Basic <base64(username:password)>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because Basic Auth sends credentials with each request, expose MCP only over HTTPS or through a TLS-terminating gateway.
|
||||
|
||||
## Authorization
|
||||
|
||||
Tools run as the authenticated Sigma user.
|
||||
Sigma applies the same resource authorization model used by the HTTP API:
|
||||
|
||||
- Root and admin users can access all MCP tools
|
||||
- Namespace readers can read namespace-scoped resources
|
||||
- Namespace managers can manage repositories, tags, and artifacts
|
||||
- Namespace admins can manage namespace metadata and members
|
||||
|
||||
Unauthorized calls return a tool error and do not execute the requested operation.
|
||||
|
||||
## Auditing and Metrics
|
||||
|
||||
Write operations are recorded in the audit table when `mcp.auditWrites` is enabled.
|
||||
Audit records use:
|
||||
|
||||
- `method`: `MCP`
|
||||
- `path`: the MCP endpoint path
|
||||
- `route`: the MCP tool name
|
||||
|
||||
Sensitive fields such as `password`, `token`, `privateKey`, `ak`, `sk`, `secret`, and `authorization` are redacted before audit storage.
|
||||
|
||||
Prometheus metrics are exposed through the existing `/metrics` endpoint:
|
||||
|
||||
- `sigma_mcp_tool_calls_total`
|
||||
- `sigma_mcp_tool_duration_seconds`
|
||||
- `sigma_mcp_auth_failures_total`
|
||||
|
||||
## Example Tool Arguments
|
||||
|
||||
List repositories in a namespace:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
Create a repository:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "library/alpine",
|
||||
"description": "Alpine images"
|
||||
}
|
||||
```
|
||||
|
||||
Get a tag manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"repository_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"digest": "sha256:..."
|
||||
}
|
||||
```
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: CLI 与 AI Agent
|
||||
---
|
||||
|
||||
# CLI 与 AI Agent
|
||||
|
||||
Sigma 提供两种从终端或 AI agent 操作远程服务端的方式。两者都调用同一套 HTTP
|
||||
API,因此 web 界面能做的事都可以脚本化。
|
||||
|
||||
| 方式 | 适用场景 |
|
||||
| --- | --- |
|
||||
| `sigma-cli` | 人、CI 流水线、可执行命令的 agent |
|
||||
| `sigma-api-operator` skill | 需要访问 CLI 尚未覆盖的 API 的 AI agent |
|
||||
|
||||
Sigma 不再内置 MCP server。无法执行命令的客户端应使用轻量 HTTP 客户端调用
|
||||
OpenAPI 文档描述的 REST API。
|
||||
|
||||
## 构建 CLI
|
||||
|
||||
```bash
|
||||
make build-cli
|
||||
./bin/sigma-cli --help
|
||||
```
|
||||
|
||||
## 登录
|
||||
|
||||
`sigma-cli` 将 Basic Auth 凭据保存在 `~/.sigma/cli-credentials.json`,权限为
|
||||
`0600`。可通过 `--credentials-file` 或 `SIGMA_CREDENTIALS_FILE` 修改位置。
|
||||
|
||||
```bash
|
||||
./bin/sigma-cli login \
|
||||
--server https://sigma.example.com \
|
||||
--username sigma \
|
||||
--password 'Admin@123'
|
||||
```
|
||||
|
||||
也可以不落盘,直接用全局参数或环境变量提供凭据:
|
||||
|
||||
```bash
|
||||
export SIGMA_SERVER=https://sigma.example.com
|
||||
export SIGMA_USERNAME=sigma
|
||||
export SIGMA_PASSWORD='Admin@123'
|
||||
```
|
||||
|
||||
全局参数:
|
||||
|
||||
| 参数 | 环境变量 | 默认值 |
|
||||
| --- | --- | --- |
|
||||
| `--server` | `SIGMA_SERVER` | `http://localhost:3000` |
|
||||
| `--credentials-file` | `SIGMA_CREDENTIALS_FILE` | `~/.sigma/cli-credentials.json` |
|
||||
| `--timeout` | – | `30s` |
|
||||
| `--debug` | – | `false` |
|
||||
|
||||
## 命令参考
|
||||
|
||||
### 命名空间
|
||||
|
||||
```bash
|
||||
sigma-cli namespace list --page 1 --limit 10
|
||||
sigma-cli namespace get <namespace_id>
|
||||
sigma-cli namespace create library --visibility public --description "基础镜像"
|
||||
sigma-cli namespace update <namespace_id> --size-limit 107374182400
|
||||
sigma-cli namespace delete <namespace_id>
|
||||
```
|
||||
|
||||
### 仓库
|
||||
|
||||
```bash
|
||||
sigma-cli repo list <namespace_id>
|
||||
sigma-cli repo get <namespace_id> <repository_id>
|
||||
sigma-cli repo create <namespace_id> library/alpine --visibility public
|
||||
sigma-cli repo update <namespace_id> <repository_id> --description "Alpine 镜像"
|
||||
sigma-cli repo delete <namespace_id> <repository_id>
|
||||
```
|
||||
|
||||
### Tag
|
||||
|
||||
```bash
|
||||
sigma-cli tag list <namespace_id> <repository_id> --type image
|
||||
sigma-cli tag get <namespace_id> <repository_id> <tag_id>
|
||||
sigma-cli tag delete <namespace_id> <repository_id> <tag_id>
|
||||
```
|
||||
|
||||
### 制品
|
||||
|
||||
制品相关命令使用命名空间和仓库的**名称**,而不是 ID。
|
||||
|
||||
```bash
|
||||
sigma-cli artifact list <namespace_name> <repository_name>
|
||||
sigma-cli artifact get <namespace_name> <repository_name> <digest>
|
||||
sigma-cli artifact delete <namespace_name> <repository_name> <digest>
|
||||
```
|
||||
|
||||
### 成员
|
||||
|
||||
```bash
|
||||
sigma-cli member list <namespace_id>
|
||||
sigma-cli member add <namespace_id> <user_id> --role reader
|
||||
sigma-cli member update <namespace_id> <user_id> --role admin
|
||||
sigma-cli member delete <namespace_id> <user_id>
|
||||
```
|
||||
|
||||
### 用户
|
||||
|
||||
```bash
|
||||
sigma-cli user list --without-admin
|
||||
sigma-cli user create --username alice --password '...' --email alice@example.com --role user
|
||||
sigma-cli user update <user_id> --role admin --status enabled
|
||||
```
|
||||
|
||||
所有命令成功时输出 JSON,失败时输出格式化后的错误响应体,因此可以直接和
|
||||
`jq` 组合使用:
|
||||
|
||||
```bash
|
||||
sigma-cli namespace list --limit 100 | jq -r '.items[].name'
|
||||
```
|
||||
|
||||
## AI Agent
|
||||
|
||||
`tools/skill/sigma-api-operator` 为 Claude Code、Copilot CLI 等 CLI agent 打包了
|
||||
一个 skill:
|
||||
|
||||
```text
|
||||
tools/skill/sigma-api-operator/SKILL.md
|
||||
tools/skill/sigma-api-operator/swagger.yaml
|
||||
```
|
||||
|
||||
该 skill 要求 agent 在发起请求前先加载 OpenAPI 文档。对于 CLI 尚未覆盖的资源
|
||||
——builder、daemon、webhook、validator、代码仓库和 token——必须通过 REST API
|
||||
调用。
|
||||
|
||||
修改 handler 后刷新打包的接口文档:
|
||||
|
||||
```bash
|
||||
make swagen
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
- 破坏性命令(`delete`、凭据与权限变更)必须由 agent 明确确认后执行。
|
||||
- 远程服务端优先使用 HTTPS;Basic Auth 凭据会随每个请求发送。
|
||||
- 不要打印密码、token、私钥或 `Authorization` 头。
|
||||
- 返回非 2xx 响应体时,先脱敏再展示给用户。
|
||||
@@ -210,14 +210,6 @@ auth:
|
||||
# fixed delay applied to every authentication attempt after maxFailures
|
||||
delay: 1s
|
||||
|
||||
mcp:
|
||||
enabled: false
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
|
||||
audit:
|
||||
# record completed authenticated business HTTP requests
|
||||
enabled: true
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
---
|
||||
title: MCP Server
|
||||
---
|
||||
|
||||
Sigma 可以在现有 `sigma server` 进程中内嵌 MCP server,用于 AI 客户端和自动化工具访问 Sigma 的管理能力。
|
||||
MCP 默认关闭,需要显式开启。
|
||||
|
||||
## 暴露哪些能力
|
||||
|
||||
MCP server 只暴露管理面和元数据能力,不暴露 OCI Blob 上传/下载流量。
|
||||
|
||||
当前已支持的 tools:
|
||||
|
||||
- 系统:`system-version-get`、`system-endpoint-get`、`system-config-summary-get`
|
||||
- Namespace:`namespace-list`、`namespace-get`、`namespace-create`、`namespace-update`、`namespace-delete`
|
||||
- Namespace 成员:`namespace-member-list`、`namespace-member-add`、`namespace-member-update`、`namespace-member-delete`、`namespace-member-self-get`
|
||||
- Repository:`repository-list`、`repository-get`、`repository-create`、`repository-update`、`repository-delete`
|
||||
- Tag:`tag-list`、`tag-get`、`tag-delete`、`tag-manifest-raw-get`
|
||||
- Artifact:`artifact-list`、`artifact-get`、`artifact-delete`
|
||||
|
||||
只读 resources:
|
||||
|
||||
- `sigma://system/version`
|
||||
- `sigma://system/capabilities`
|
||||
|
||||
## 开启 MCP
|
||||
|
||||
在配置文件中设置 `mcp`:
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
enabled: true
|
||||
path: /api/v1/mcp
|
||||
transport: streamable_http
|
||||
auditWrites: true
|
||||
toolTimeout: 30s
|
||||
maxRequestBody: 1048576
|
||||
```
|
||||
|
||||
使用 Helm 部署时:
|
||||
|
||||
```bash
|
||||
helm upgrade --install sigma ./deploy/sigma \
|
||||
--set config.mcp.enabled=true
|
||||
```
|
||||
|
||||
MCP endpoint 复用现有 server service:
|
||||
|
||||
```text
|
||||
https://<sigma-host>/api/v1/mcp
|
||||
```
|
||||
|
||||
## 认证方式
|
||||
|
||||
MCP 使用 Sigma Basic Auth。每个 MCP 请求都必须携带:
|
||||
|
||||
```text
|
||||
Authorization: Basic <base64(username:password)>
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```bash
|
||||
printf 'sigma:Admin@123' | base64
|
||||
```
|
||||
|
||||
将编码后的值写入 MCP client 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sigma": {
|
||||
"url": "https://<sigma-host>/api/v1/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Basic <base64(username:password)>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Basic Auth 会在每次请求中发送凭据,因此 MCP 必须通过 HTTPS 或具备 TLS 终止能力的网关暴露。
|
||||
|
||||
## 权限模型
|
||||
|
||||
MCP tool 会以认证后的 Sigma 用户身份执行,并复用 Sigma HTTP API 的资源权限模型:
|
||||
|
||||
- Root 和 admin 用户可以访问所有 MCP tools
|
||||
- Namespace reader 可以读取 namespace 范围内的资源
|
||||
- Namespace manager 可以管理 repository、tag 和 artifact
|
||||
- Namespace admin 可以管理 namespace 元数据和成员
|
||||
|
||||
没有权限的调用会返回 tool error,不会执行对应操作。
|
||||
|
||||
## 审计和指标
|
||||
|
||||
当 `mcp.auditWrites` 开启时,写操作会记录到 audit 表。
|
||||
审计字段使用:
|
||||
|
||||
- `method`: `MCP`
|
||||
- `path`: MCP endpoint path
|
||||
- `route`: MCP tool name
|
||||
|
||||
写入审计前会过滤敏感字段,例如 `password`、`token`、`privateKey`、`ak`、`sk`、`secret`、`authorization`。
|
||||
|
||||
MCP 指标复用现有 `/metrics` endpoint 暴露:
|
||||
|
||||
- `sigma_mcp_tool_calls_total`
|
||||
- `sigma_mcp_tool_duration_seconds`
|
||||
- `sigma_mcp_auth_failures_total`
|
||||
|
||||
## Tool 参数示例
|
||||
|
||||
列出 namespace 下的 repositories:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
创建 repository:
|
||||
|
||||
```json
|
||||
{
|
||||
"namespace_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "library/alpine",
|
||||
"description": "Alpine images"
|
||||
}
|
||||
```
|
||||
|
||||
获取 tag manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"repository_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"digest": "sha256:..."
|
||||
}
|
||||
```
|
||||
@@ -41,7 +41,6 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/lmittmann/tint v1.2.1
|
||||
github.com/mark3labs/mcp-go v1.2.1
|
||||
github.com/moby/buildkit v0.34.1
|
||||
github.com/opencontainers/distribution-spec/specs-go v0.0.0-20260831232431-97274622c111
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
@@ -289,7 +288,6 @@ require (
|
||||
github.com/google/go-containerregistry v0.22.1 // indirect
|
||||
github.com/google/go-intervals v0.0.2 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||
github.com/google/licensecheck v0.3.1 // indirect
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect
|
||||
github.com/google/s2a-go v0.1.11 // indirect
|
||||
@@ -420,7 +418,6 @@ require (
|
||||
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
|
||||
github.com/sassoftware/go-rpmutils v0.4.0 // indirect
|
||||
github.com/sassoftware/relic/v8 v8.2.0 // indirect
|
||||
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect
|
||||
@@ -483,7 +480,6 @@ require (
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.2 // indirect
|
||||
github.com/zclconf/go-cty v1.19.0 // indirect
|
||||
|
||||
@@ -450,8 +450,6 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v29.9.2+incompatible h1:trup/vUxx5uUo2t+87Cd9AmITR+DvKUe4H2tTtZN8FY=
|
||||
github.com/docker/cli v29.9.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
@@ -781,8 +779,6 @@ github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJ
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||
github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=
|
||||
github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
@@ -1075,8 +1071,6 @@ github.com/magiconair/properties v1.18.12/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mark3labs/mcp-go v1.2.1 h1:gd8VD3feMAVycwAM0x1nhv42Q1oLLjfOyowoUzHULB8=
|
||||
github.com/mark3labs/mcp-go v1.2.1/go.mod h1:r2fW4o3wsoJ7IMsx1Wuq5xeP8PRGXPDfNveoGAYbb/s=
|
||||
github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
|
||||
github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s=
|
||||
@@ -1590,8 +1584,6 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAz
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/ysmood/fetchup v0.3.0 h1:UhYz9xnLEVn2ukSuK3KCgcznWpHMdrmbsPpllcylyu8=
|
||||
|
||||
@@ -219,9 +219,6 @@ func genAuthzSkipper() authz.Skipper {
|
||||
if !(strings.HasPrefix(requestUri, "/v2/") || strings.HasPrefix(requestUri, consts.APIV1)) { // nolint: staticcheck
|
||||
return true
|
||||
}
|
||||
if config.GetConfig().MCP.Enabled && requestUri == config.GetConfig().MCP.Path {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(skipAuthzs, strings.ToLower(fmt.Sprintf("%s:%s", requestMethod, requestUri)))
|
||||
}
|
||||
}
|
||||
@@ -229,9 +226,6 @@ func genAuthzSkipper() authz.Skipper {
|
||||
func genAuditSkipper() audit.Skipper {
|
||||
return func(c *gin.Context) bool {
|
||||
requestPath := c.Request.URL.Path
|
||||
if config.GetConfig().MCP.Enabled && requestPath == config.GetConfig().MCP.Path {
|
||||
return true
|
||||
}
|
||||
return !strings.HasPrefix(requestPath, "/v2/") && requestPath != "/v2" && !strings.HasPrefix(requestPath, consts.APIV1)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ import (
|
||||
"github.com/go-sigma/sigma/pkg/infra/workq"
|
||||
"github.com/go-sigma/sigma/pkg/logger"
|
||||
"github.com/go-sigma/sigma/pkg/server/handlers"
|
||||
mcpserver "github.com/go-sigma/sigma/pkg/server/mcp"
|
||||
)
|
||||
|
||||
// ServerConfig selects which optional subsystems Serve initializes; each
|
||||
@@ -109,10 +108,6 @@ func Serve(digCon *dig.Container) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mcpserver.Register(digCon)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize mcp server: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: consts.ServerPort,
|
||||
|
||||
@@ -88,13 +88,12 @@ func resolveV2(path string) (ResourceDescriptor, error) {
|
||||
return ResourceDescriptor{NamespaceName: firstSegment(repo), Resource: ResBlobUploads}, nil
|
||||
}
|
||||
|
||||
// Split into [repo...suffix, lastSegment] using the last "/".
|
||||
idx := strings.LastIndex(rest, "/")
|
||||
if idx < 0 {
|
||||
// Split into [repo...suffix, lastSegment] using the last "/", where prefix
|
||||
// keeps the repo plus the suffix marker.
|
||||
prefix, ref, ok := strings.CutLast(rest, "/")
|
||||
if !ok {
|
||||
return ResourceDescriptor{}, fmt.Errorf("unrecognized /v2 path: %s", path)
|
||||
}
|
||||
ref := rest[idx+1:]
|
||||
prefix := rest[:idx] // repo + suffix marker
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(prefix, "/manifests"):
|
||||
|
||||
@@ -130,34 +130,6 @@ http:
|
||||
require.Equal(t, 30*time.Second, cfg.HTTP.Timeout.IdleTimeout())
|
||||
}
|
||||
|
||||
func TestConfigurationMCPWithDefaults(t *testing.T) {
|
||||
var mcp ConfigurationMCP
|
||||
mcp.WithDefaults()
|
||||
require.False(t, mcp.Enabled)
|
||||
require.Equal(t, "/api/v1/mcp", mcp.Path)
|
||||
require.Equal(t, "streamable_http", mcp.Transport)
|
||||
require.True(t, mcp.AuditWritesEnabled())
|
||||
require.Equal(t, 30*time.Second, mcp.ToolTimeout)
|
||||
require.Equal(t, int64(1<<20), mcp.MaxRequestBody)
|
||||
|
||||
disabled := false
|
||||
mcp = ConfigurationMCP{
|
||||
Enabled: true,
|
||||
Path: "/custom/mcp",
|
||||
Transport: "sse",
|
||||
AuditWrites: &disabled,
|
||||
ToolTimeout: time.Minute,
|
||||
MaxRequestBody: 2 << 20,
|
||||
}
|
||||
mcp.WithDefaults()
|
||||
require.True(t, mcp.Enabled)
|
||||
require.Equal(t, "/custom/mcp", mcp.Path)
|
||||
require.Equal(t, "sse", mcp.Transport)
|
||||
require.False(t, mcp.AuditWritesEnabled())
|
||||
require.Equal(t, time.Minute, mcp.ToolTimeout)
|
||||
require.Equal(t, int64(2<<20), mcp.MaxRequestBody)
|
||||
}
|
||||
|
||||
func TestConfigurationApplyEnvOverrides(t *testing.T) {
|
||||
t.Setenv("SIGMA_DATABASE_MYSQL_PASSWORD", "mysql-env")
|
||||
t.Setenv("SIGMA_DATABASE_POSTGRESQL_PASSWORD", "postgresql-env")
|
||||
|
||||
@@ -32,11 +32,6 @@ const (
|
||||
defaultHTTPTimeoutWrite = time.Hour
|
||||
defaultHTTPTimeoutIdle = 2 * time.Minute
|
||||
|
||||
defaultMCPPath = "/api/v1/mcp"
|
||||
defaultMCPTransport = "streamable_http"
|
||||
defaultMCPToolTimeout = 30 * time.Second
|
||||
defaultMCPMaxRequestBody = int64(1 << 20) // 1Mi
|
||||
|
||||
defaultAuthLoginRateLimitMaxFailures = 5
|
||||
defaultAuthLoginRateLimitWindow = 5 * time.Minute
|
||||
defaultAuthLoginRateLimitDelay = time.Second
|
||||
@@ -70,7 +65,6 @@ type Configuration struct {
|
||||
Daemon ConfigurationDaemon `yaml:"daemon"`
|
||||
Auth ConfigurationAuth `yaml:"auth"`
|
||||
Audit ConfigurationAudit `yaml:"audit"`
|
||||
MCP ConfigurationMCP `yaml:"mcp"`
|
||||
Trace ConfigurationTrace `yaml:"trace"`
|
||||
Analytics ConfigurationAnalytics `yaml:"analytics"`
|
||||
}
|
||||
@@ -83,48 +77,12 @@ func (c *Configuration) WithDefaults() {
|
||||
c.Namespace.WithDefaults()
|
||||
c.Daemon.WithDefaults()
|
||||
c.Audit.Cleanup.WithDefaults()
|
||||
c.MCP.WithDefaults()
|
||||
c.WorkQueue.Inmemory.WithDefaults()
|
||||
c.Cache.WithDefaults()
|
||||
c.Locker.WithDefaults()
|
||||
c.Database.WithDefaults()
|
||||
}
|
||||
|
||||
// ConfigurationMCP configures the embedded MCP server.
|
||||
type ConfigurationMCP struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Path string `yaml:"path"`
|
||||
Transport string `yaml:"transport"`
|
||||
AuditWrites *bool `yaml:"auditWrites"`
|
||||
ToolTimeout time.Duration `yaml:"toolTimeout"`
|
||||
MaxRequestBody int64 `yaml:"maxRequestBody"`
|
||||
}
|
||||
|
||||
// WithDefaults applies default values for MCP.
|
||||
func (c *ConfigurationMCP) WithDefaults() {
|
||||
if c.Path == "" {
|
||||
c.Path = defaultMCPPath
|
||||
}
|
||||
if c.Transport == "" {
|
||||
c.Transport = defaultMCPTransport
|
||||
}
|
||||
if c.AuditWrites == nil {
|
||||
enabled := true
|
||||
c.AuditWrites = &enabled
|
||||
}
|
||||
if c.ToolTimeout == 0 {
|
||||
c.ToolTimeout = defaultMCPToolTimeout
|
||||
}
|
||||
if c.MaxRequestBody == 0 {
|
||||
c.MaxRequestBody = defaultMCPMaxRequestBody
|
||||
}
|
||||
}
|
||||
|
||||
// AuditWritesEnabled reports whether write MCP tool calls should be audited.
|
||||
func (c ConfigurationMCP) AuditWritesEnabled() bool {
|
||||
return c.AuditWrites == nil || *c.AuditWrites
|
||||
}
|
||||
|
||||
// ConfigurationAudit configures HTTP audit recording.
|
||||
type ConfigurationAudit struct {
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
|
||||
@@ -66,15 +66,15 @@ func replaceLogAttr(_ []string, attr slog.Attr) slog.Attr {
|
||||
}
|
||||
|
||||
func trimSourceFile(file string) string {
|
||||
lastSlash := strings.LastIndex(file, "/")
|
||||
if lastSlash < 0 {
|
||||
dir, base, ok := strings.CutLast(file, "/")
|
||||
if !ok {
|
||||
return file
|
||||
}
|
||||
prevSlash := strings.LastIndex(file[:lastSlash], "/")
|
||||
if prevSlash < 0 {
|
||||
_, parent, ok := strings.CutLast(dir, "/")
|
||||
if !ok {
|
||||
return file
|
||||
}
|
||||
return file[prevSlash+1:]
|
||||
return parent + "/" + base
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json/v2"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
repoaudit "github.com/go-sigma/sigma/pkg/dal/repository/audit"
|
||||
"github.com/go-sigma/sigma/pkg/utils/uuid"
|
||||
)
|
||||
|
||||
var sensitiveKeys = map[string]struct{}{
|
||||
"ak": {},
|
||||
"authorization": {},
|
||||
"password": {},
|
||||
"privatekey": {},
|
||||
"scmpassword": {},
|
||||
"scmsshkey": {},
|
||||
"scmtoken": {},
|
||||
"secret": {},
|
||||
"sk": {},
|
||||
"token": {},
|
||||
}
|
||||
|
||||
func auditToolCall(
|
||||
ctx context.Context,
|
||||
repo repoaudit.AuditRepository,
|
||||
path string,
|
||||
user *models.User,
|
||||
toolName string,
|
||||
request mcp.CallToolRequest,
|
||||
startedAt time.Time,
|
||||
statusCode int,
|
||||
) {
|
||||
|
||||
if repo == nil || user == nil {
|
||||
return
|
||||
}
|
||||
clientIP, userAgent := clientInfoFromContext(ctx)
|
||||
args := request.GetArguments()
|
||||
query, err := json.Marshal(redactSensitive(args))
|
||||
if err != nil {
|
||||
slog.Error("marshal mcp audit arguments failed", "err", err, "tool", toolName)
|
||||
query = []byte("{}")
|
||||
}
|
||||
auditObj := &models.Audit{
|
||||
ID: uuid.NewV7String(),
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
UserRole: user.Role.String(),
|
||||
Method: "MCP",
|
||||
Path: path,
|
||||
Route: toolName,
|
||||
Query: string(query),
|
||||
StatusCode: statusCode,
|
||||
ClientIP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
LatencyMs: time.Since(startedAt).Milliseconds(),
|
||||
}
|
||||
if namespaceID := stringArgument(args, "namespace_id", "namespaceID"); namespaceID != "" {
|
||||
auditObj.NamespaceID = &namespaceID
|
||||
}
|
||||
if e := repo.Create(ctx, auditObj); e != nil {
|
||||
slog.Error("create mcp audit failed", "err", e, "tool", toolName)
|
||||
}
|
||||
}
|
||||
|
||||
func redactSensitive(value any) any {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
result := make(map[string]any, len(v))
|
||||
for key, item := range v {
|
||||
if _, ok := sensitiveKeys[normalizeKey(key)]; ok {
|
||||
result[key] = "[REDACTED]"
|
||||
continue
|
||||
}
|
||||
result[key] = redactSensitive(item)
|
||||
}
|
||||
return result
|
||||
case []any:
|
||||
result := make([]any, 0, len(v))
|
||||
for _, item := range v {
|
||||
result = append(result, redactSensitive(item))
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeKey(key string) string {
|
||||
replacer := strings.NewReplacer("_", "", "-", "", ".", "")
|
||||
return strings.ToLower(replacer.Replace(key))
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRedactSensitive(t *testing.T) {
|
||||
input := map[string]any{
|
||||
"username": "sigma",
|
||||
"password": "secret",
|
||||
"scm_token": "token",
|
||||
"private_key": "key",
|
||||
"nested": map[string]any{
|
||||
"authorization": "Basic value",
|
||||
"value": "kept",
|
||||
},
|
||||
"items": []any{
|
||||
map[string]any{"sk": "secret-key"},
|
||||
},
|
||||
}
|
||||
|
||||
redacted, ok := redactSensitive(input).(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "sigma", redacted["username"])
|
||||
require.Equal(t, "[REDACTED]", redacted["password"])
|
||||
require.Equal(t, "[REDACTED]", redacted["scm_token"])
|
||||
require.Equal(t, "[REDACTED]", redacted["private_key"])
|
||||
|
||||
nested, ok := redacted["nested"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "[REDACTED]", nested["authorization"])
|
||||
require.Equal(t, "kept", nested["value"])
|
||||
|
||||
items, ok := redacted["items"].([]any)
|
||||
require.True(t, ok)
|
||||
item, ok := items[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "[REDACTED]", item["sk"])
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
)
|
||||
|
||||
func (s *Server) authorizeNamespace(ctx context.Context, user *models.User, namespaceID string, auth enums.Auth) error {
|
||||
allowed, err := s.authorizer.Namespace(ctx, *user, namespaceID, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
return errForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) authorizeRepository(ctx context.Context, user *models.User, repositoryID string, auth enums.Auth) error {
|
||||
allowed, err := s.authorizer.Repository(ctx, *user, repositoryID, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
return errForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) authorizeTag(ctx context.Context, user *models.User, tagID string, auth enums.Auth) error {
|
||||
allowed, err := s.authorizer.Tag(ctx, *user, tagID, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
return errForbidden
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/authz"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
)
|
||||
|
||||
func TestAuthorizeHelpers(t *testing.T) {
|
||||
expectedErr := errors.New("lookup failed")
|
||||
user := &models.User{ID: "user-1"}
|
||||
tests := []struct {
|
||||
name string
|
||||
authorizer authz.Authorizer
|
||||
invoke func(*Server) error
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "namespace allowed",
|
||||
authorizer: fakeAuthorizer{namespaceAllowed: true},
|
||||
invoke: func(server *Server) error {
|
||||
return server.authorizeNamespace(t.Context(), user, "namespace-1", enums.AuthRead)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "namespace denied",
|
||||
authorizer: fakeAuthorizer{},
|
||||
invoke: func(server *Server) error {
|
||||
return server.authorizeNamespace(t.Context(), user, "namespace-1", enums.AuthRead)
|
||||
},
|
||||
wantErr: errForbidden,
|
||||
},
|
||||
{
|
||||
name: "repository error",
|
||||
authorizer: fakeAuthorizer{repositoryErr: expectedErr},
|
||||
invoke: func(server *Server) error {
|
||||
return server.authorizeRepository(t.Context(), user, "repository-1", enums.AuthRead)
|
||||
},
|
||||
wantErr: expectedErr,
|
||||
},
|
||||
{
|
||||
name: "repository allowed",
|
||||
authorizer: fakeAuthorizer{repositoryAllowed: true},
|
||||
invoke: func(server *Server) error {
|
||||
return server.authorizeRepository(t.Context(), user, "repository-1", enums.AuthManage)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tag denied",
|
||||
authorizer: fakeAuthorizer{},
|
||||
invoke: func(server *Server) error { return server.authorizeTag(t.Context(), user, "tag-1", enums.AuthRead) },
|
||||
wantErr: errForbidden,
|
||||
},
|
||||
{
|
||||
name: "tag allowed",
|
||||
authorizer: fakeAuthorizer{tagAllowed: true},
|
||||
invoke: func(server *Server) error { return server.authorizeTag(t.Context(), user, "tag-1", enums.AuthAdmin) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.invoke(&Server{authorizer: tt.authorizer})
|
||||
if tt.wantErr == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
namespaceAllowed bool
|
||||
repositoryAllowed bool
|
||||
tagAllowed bool
|
||||
repositoryErr error
|
||||
}
|
||||
|
||||
var _ authz.Authorizer = fakeAuthorizer{}
|
||||
|
||||
func (fakeAuthorizer) Authorize(context.Context, string, bool, string, string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Namespace(context.Context, models.User, string, enums.Auth) (bool, error) {
|
||||
return f.namespaceAllowed, nil
|
||||
}
|
||||
|
||||
func (fakeAuthorizer) NamespaceRole(context.Context, models.User, string) (*enums.NamespaceRole, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (fakeAuthorizer) NamespacesRole(context.Context, models.User, []string) (map[string]*enums.NamespaceRole, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Repository(context.Context, models.User, string, enums.Auth) (bool, error) {
|
||||
return f.repositoryAllowed, f.repositoryErr
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Tag(context.Context, models.User, string, enums.Auth) (bool, error) {
|
||||
return f.tagAllowed, nil
|
||||
}
|
||||
|
||||
func (fakeAuthorizer) Artifact(context.Context, models.User, string, enums.Auth) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
artifactsmocks "github.com/go-sigma/sigma/pkg/service/artifacts/mocks"
|
||||
namespacesmocks "github.com/go-sigma/sigma/pkg/service/namespaces/mocks"
|
||||
repositoriesmocks "github.com/go-sigma/sigma/pkg/service/repositories/mocks"
|
||||
tagsmocks "github.com/go-sigma/sigma/pkg/service/tags/mocks"
|
||||
)
|
||||
|
||||
func TestNamespaceTools(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
namespaceSvc := namespacesmocks.NewMockNamespaceService(ctrl)
|
||||
server := &Server{
|
||||
authorizer: fakeAuthorizer{namespaceAllowed: true},
|
||||
namespaceSvc: namespaceSvc,
|
||||
}
|
||||
ctx := withUser(t.Context(), &models.User{ID: "user-1"})
|
||||
|
||||
namespaceSvc.EXPECT().
|
||||
ListNamespaces(gomock.Any(), "user-1", nil, api.Pagination{}, api.Sortable{}).
|
||||
Return([]*models.Namespace{{ID: "namespace-1"}}, int64(1), nil)
|
||||
result, err := server.namespaceList(ctx, toolRequest(nil))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(1), result.(map[string]any)["total"])
|
||||
|
||||
request := api.PostNamespaceRequest{Name: "sigma"}
|
||||
namespaceSvc.EXPECT().
|
||||
CreateNamespace(gomock.Any(), "user-1", request).
|
||||
Return(&models.Namespace{ID: "namespace-1"}, nil)
|
||||
result, err = server.namespaceCreate(ctx, toolRequest(map[string]any{"name": "sigma"}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{"id": "namespace-1"}, result)
|
||||
|
||||
namespaceSvc.EXPECT().
|
||||
DeleteNamespace(gomock.Any(), "user-1", "namespace-1").
|
||||
Return(nil)
|
||||
result, err = server.namespaceDelete(ctx, toolRequest(map[string]any{"id": "namespace-1"}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, okResponse(), result)
|
||||
}
|
||||
|
||||
func TestRepositoryTools(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
repositorySvc := repositoriesmocks.NewMockRepositoryService(ctrl)
|
||||
server := &Server{
|
||||
authorizer: fakeAuthorizer{namespaceAllowed: true, repositoryAllowed: true},
|
||||
repositorySvc: repositorySvc,
|
||||
}
|
||||
ctx := withUser(t.Context(), &models.User{ID: "user-1"})
|
||||
|
||||
request := api.CreateRepositoryRequest{NamespaceID: "namespace-1", Name: "sigma/demo"}
|
||||
repositorySvc.EXPECT().
|
||||
CreateRepository(gomock.Any(), "user-1", request).
|
||||
Return(&models.Repository{ID: "repository-1"}, nil)
|
||||
result, err := server.repositoryCreate(ctx, toolRequest(map[string]any{
|
||||
"namespace_id": "namespace-1",
|
||||
"name": "sigma/demo",
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{"id": "repository-1"}, result)
|
||||
|
||||
repositorySvc.EXPECT().
|
||||
GetRepository(gomock.Any(), "repository-1").
|
||||
Return(&models.Repository{ID: "repository-1"}, nil)
|
||||
result, err = server.repositoryGet(ctx, toolRequest(map[string]any{"id": "repository-1"}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &models.Repository{ID: "repository-1"}, result)
|
||||
}
|
||||
|
||||
func TestTagManifestRawGet(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
tagSvc := tagsmocks.NewMockTagService(ctrl)
|
||||
server := &Server{
|
||||
authorizer: fakeAuthorizer{repositoryAllowed: true},
|
||||
tagSvc: tagSvc,
|
||||
}
|
||||
ctx := withUser(t.Context(), &models.User{ID: "user-1"})
|
||||
|
||||
tagSvc.EXPECT().
|
||||
GetArtifactRaw(gomock.Any(), "sha256:digest").
|
||||
Return([]byte(`{"schemaVersion":2}`), nil)
|
||||
result, err := server.tagManifestRawGet(ctx, toolRequest(map[string]any{
|
||||
"repository_id": "repository-1",
|
||||
"digest": "sha256:digest",
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{"raw": `{"schemaVersion":2}`}, result)
|
||||
}
|
||||
|
||||
func TestArtifactTools(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
repositorySvc := repositoriesmocks.NewMockRepositoryService(ctrl)
|
||||
artifactSvc := artifactsmocks.NewMockArtifactService(ctrl)
|
||||
server := &Server{
|
||||
authorizer: fakeAuthorizer{repositoryAllowed: true},
|
||||
repositorySvc: repositorySvc,
|
||||
artifactSvc: artifactSvc,
|
||||
}
|
||||
ctx := withUser(t.Context(), &models.User{ID: "user-1"})
|
||||
|
||||
repositorySvc.EXPECT().
|
||||
GetRepositoryByName(gomock.Any(), "sigma/demo").
|
||||
Return(&models.Repository{ID: "repository-1"}, nil)
|
||||
artifactSvc.EXPECT().
|
||||
GetArtifact(gomock.Any(), "sigma/demo", "sha256:digest").
|
||||
Return(&models.Artifact{ID: "artifact-1"}, nil)
|
||||
result, err := server.artifactGet(ctx, toolRequest(map[string]any{
|
||||
"repository": "sigma/demo",
|
||||
"digest": "sha256:digest",
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &models.Artifact{ID: "artifact-1"}, result)
|
||||
|
||||
repositorySvc.EXPECT().
|
||||
GetRepositoryByName(gomock.Any(), "sigma/demo").
|
||||
Return(&models.Repository{ID: "repository-1"}, nil)
|
||||
artifactSvc.EXPECT().
|
||||
DeleteArtifact(gomock.Any(), "sigma/demo", "sha256:digest").
|
||||
Return(nil)
|
||||
result, err = server.artifactDelete(ctx, toolRequest(map[string]any{
|
||||
"repository": "sigma/demo",
|
||||
"digest": "sha256:digest",
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, okResponse(), result)
|
||||
}
|
||||
|
||||
func toolRequest(arguments map[string]any) mcp.CallToolRequest {
|
||||
return mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{Arguments: arguments},
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
contextKeyUser contextKey = "sigma.mcp.user"
|
||||
contextKeyClientIP contextKey = "sigma.mcp.client_ip"
|
||||
contextKeyUserAgent contextKey = "sigma.mcp.user_agent"
|
||||
)
|
||||
|
||||
func withUser(ctx context.Context, user *models.User) context.Context {
|
||||
return context.WithValue(ctx, contextKeyUser, user)
|
||||
}
|
||||
|
||||
func userFromContext(ctx context.Context) (*models.User, bool) {
|
||||
user, ok := ctx.Value(contextKeyUser).(*models.User)
|
||||
return user, ok && user != nil
|
||||
}
|
||||
|
||||
func withClientInfo(ctx context.Context, clientIP, userAgent string) context.Context {
|
||||
ctx = context.WithValue(ctx, contextKeyClientIP, clientIP)
|
||||
return context.WithValue(ctx, contextKeyUserAgent, userAgent)
|
||||
}
|
||||
|
||||
func clientInfoFromContext(ctx context.Context) (string, string) {
|
||||
clientIP, _ := ctx.Value(contextKeyClientIP).(string)
|
||||
userAgent, _ := ctx.Value(contextKeyUserAgent).(string)
|
||||
return clientIP, userAgent
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
)
|
||||
|
||||
var (
|
||||
errUnauthorized = errors.New("unauthorized")
|
||||
errForbidden = errors.New("permission denied")
|
||||
)
|
||||
|
||||
func toolErrorMessage(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if errors.Is(err, errUnauthorized) {
|
||||
return errUnauthorized.Error()
|
||||
}
|
||||
if errors.Is(err, errForbidden) {
|
||||
return errForbidden.Error()
|
||||
}
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
if e.Description != "" {
|
||||
return e.Description
|
||||
}
|
||||
if e.Title != "" {
|
||||
return e.Title
|
||||
}
|
||||
return e.Code
|
||||
}
|
||||
return "internal error"
|
||||
}
|
||||
|
||||
func statusCodeFromError(err error) int {
|
||||
if err == nil {
|
||||
return 200
|
||||
}
|
||||
if errors.Is(err, errUnauthorized) || errors.Is(err, errForbidden) {
|
||||
return 401
|
||||
}
|
||||
if e, ok := errcode.AsType[errcode.ErrCode](err); ok {
|
||||
return e.HTTPStatusCode
|
||||
}
|
||||
return 500
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
)
|
||||
|
||||
func TestContextValues(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, ok := userFromContext(ctx)
|
||||
require.False(t, ok)
|
||||
|
||||
user := &models.User{ID: "user-1"}
|
||||
ctx = withUser(ctx, user)
|
||||
got, ok := userFromContext(ctx)
|
||||
require.True(t, ok)
|
||||
require.Same(t, user, got)
|
||||
|
||||
ctx = withClientInfo(ctx, "127.0.0.1", "sigma-test")
|
||||
clientIP, userAgent := clientInfoFromContext(ctx)
|
||||
require.Equal(t, "127.0.0.1", clientIP)
|
||||
require.Equal(t, "sigma-test", userAgent)
|
||||
}
|
||||
|
||||
func TestToolErrorMapping(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
wantText string
|
||||
wantStatus int
|
||||
}{
|
||||
{name: "nil", wantText: "", wantStatus: http.StatusOK},
|
||||
{name: "unauthorized", err: errUnauthorized, wantText: "unauthorized", wantStatus: http.StatusUnauthorized},
|
||||
{name: "forbidden", err: errForbidden, wantText: "permission denied", wantStatus: http.StatusUnauthorized},
|
||||
{
|
||||
name: "typed error",
|
||||
err: errcode.HTTPErrCodeBadRequest.Detail("invalid input"),
|
||||
wantText: "invalid input",
|
||||
wantStatus: http.StatusBadRequest,
|
||||
},
|
||||
{name: "internal", err: errors.New("sensitive detail"), wantText: "internal error", wantStatus: http.StatusInternalServerError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantText, toolErrorMessage(tt.err))
|
||||
require.Equal(t, tt.wantStatus, statusCodeFromError(tt.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponseHelpers(t *testing.T) {
|
||||
page := 2
|
||||
limit := 20
|
||||
pagination := api.Pagination{Page: &page, Limit: &limit}
|
||||
|
||||
response := listResponse([]string{"one"}, 1, pagination)
|
||||
require.Equal(t, []string{"one"}, response["items"])
|
||||
require.Equal(t, int64(1), response["total"])
|
||||
require.Equal(t, pagination, response["pagination"])
|
||||
require.Equal(t, map[string]bool{"ok": true}, okResponse())
|
||||
}
|
||||
|
||||
func TestToolArgumentHelpers(t *testing.T) {
|
||||
req := mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Arguments: map[string]any{"name": "sigma", "count": float64(3)},
|
||||
},
|
||||
}
|
||||
type arguments struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
got, err := bindArguments[arguments](req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, arguments{Name: "sigma", Count: 3}, got)
|
||||
|
||||
invalid := mcp.CallToolRequest{Params: mcp.CallToolParams{Arguments: map[string]any{"count": "invalid"}}}
|
||||
_, err = bindArguments[arguments](invalid)
|
||||
require.Error(t, err)
|
||||
|
||||
require.Equal(t, "second", stringArgument(map[string]any{
|
||||
"first": 123,
|
||||
"second": "second",
|
||||
}, "first", "second"))
|
||||
require.Empty(t, stringArgument(nil, "missing"))
|
||||
}
|
||||
|
||||
func TestNormalizeKey(t *testing.T) {
|
||||
require.Equal(t, "authorization", normalizeKey("Authorization"))
|
||||
require.Equal(t, "privatekey", normalizeKey("private_key"))
|
||||
require.Equal(t, "scmtoken", normalizeKey("scm-token"))
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
toolResultOK = "ok"
|
||||
toolResultError = "error"
|
||||
)
|
||||
|
||||
var (
|
||||
mcpToolCallsTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "sigma_mcp_tool_calls_total",
|
||||
Help: "Total number of MCP tool calls.",
|
||||
},
|
||||
[]string{"tool", "result"},
|
||||
)
|
||||
mcpToolDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "sigma_mcp_tool_duration_seconds",
|
||||
Help: "MCP tool call duration in seconds.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
[]string{"tool"},
|
||||
)
|
||||
mcpAuthFailuresTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "sigma_mcp_auth_failures_total",
|
||||
Help: "Total number of MCP authentication failures.",
|
||||
},
|
||||
[]string{"reason"},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(mcpToolCallsTotal, mcpToolDuration, mcpAuthFailuresTotal)
|
||||
}
|
||||
|
||||
func recordToolCall(toolName, result string, startedAt time.Time) {
|
||||
mcpToolCallsTotal.WithLabelValues(toolName, result).Inc()
|
||||
mcpToolDuration.WithLabelValues(toolName).Observe(time.Since(startedAt).Seconds())
|
||||
}
|
||||
|
||||
func recordAuthFailure(reason string) {
|
||||
mcpAuthFailuresTotal.WithLabelValues(reason).Inc()
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json/v2"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func (s *Server) registerResources(mcpServer *mcpsdk.MCPServer) {
|
||||
mcpServer.AddResource(
|
||||
mcp.NewResource(
|
||||
"sigma://system/version",
|
||||
"Sigma Version",
|
||||
mcp.WithMIMEType("application/json"),
|
||||
mcp.WithResourceDescription("Sigma build version information"),
|
||||
),
|
||||
s.systemVersionResource,
|
||||
)
|
||||
mcpServer.AddResource(
|
||||
mcp.NewResource(
|
||||
"sigma://system/capabilities",
|
||||
"Sigma MCP Capabilities",
|
||||
mcp.WithMIMEType("application/json"),
|
||||
mcp.WithResourceDescription("The MCP tools exposed by Sigma"),
|
||||
),
|
||||
s.capabilitiesResource,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) systemVersionResource(ctx context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
||||
version, err := s.systemsSvc.GetVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonResource("sigma://system/version", version)
|
||||
}
|
||||
|
||||
func (s *Server) capabilitiesResource(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
|
||||
capabilities := map[string]any{
|
||||
"tools": []string{
|
||||
"system-version-get",
|
||||
"system-endpoint-get",
|
||||
"system-config-summary-get",
|
||||
"namespace-list",
|
||||
"namespace-get",
|
||||
"namespace-create",
|
||||
"namespace-update",
|
||||
"namespace-delete",
|
||||
"namespace-member-list",
|
||||
"namespace-member-add",
|
||||
"namespace-member-update",
|
||||
"namespace-member-delete",
|
||||
"namespace-member-self-get",
|
||||
"repository-list",
|
||||
"repository-get",
|
||||
"repository-create",
|
||||
"repository-update",
|
||||
"repository-delete",
|
||||
"tag-list",
|
||||
"tag-get",
|
||||
"tag-delete",
|
||||
"tag-manifest-raw-get",
|
||||
"artifact-list",
|
||||
"artifact-get",
|
||||
"artifact-delete",
|
||||
},
|
||||
"auth": map[string]any{
|
||||
"type": "basic",
|
||||
},
|
||||
}
|
||||
return jsonResource("sigma://system/capabilities", capabilities)
|
||||
}
|
||||
|
||||
func jsonResource(uri string, value any) ([]mcp.ResourceContents, error) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: uri,
|
||||
MIMEType: "application/json",
|
||||
Text: string(data),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import "github.com/go-sigma/sigma/pkg/api"
|
||||
|
||||
func listResponse(items any, total int64, pagination api.Pagination) map[string]any {
|
||||
return map[string]any{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"pagination": pagination,
|
||||
}
|
||||
}
|
||||
|
||||
func okResponse() map[string]bool {
|
||||
return map[string]bool{"ok": true}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/authz"
|
||||
"github.com/go-sigma/sigma/pkg/config"
|
||||
"github.com/go-sigma/sigma/pkg/consts"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
repoaudit "github.com/go-sigma/sigma/pkg/dal/repository/audit"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
svcartifacts "github.com/go-sigma/sigma/pkg/service/artifacts"
|
||||
svcnamespaces "github.com/go-sigma/sigma/pkg/service/namespaces"
|
||||
svcrepositories "github.com/go-sigma/sigma/pkg/service/repositories"
|
||||
svcsystems "github.com/go-sigma/sigma/pkg/service/systems"
|
||||
svctags "github.com/go-sigma/sigma/pkg/service/tags"
|
||||
"github.com/go-sigma/sigma/pkg/utils"
|
||||
"github.com/go-sigma/sigma/pkg/version"
|
||||
)
|
||||
|
||||
// Params declares dependencies needed to register the MCP server.
|
||||
type Params struct {
|
||||
dig.In
|
||||
|
||||
Config *config.Configuration
|
||||
Engine *gin.Engine
|
||||
AuditRepository repoaudit.AuditRepository
|
||||
Authorizer authz.Authorizer
|
||||
SystemsSvc svcsystems.Service
|
||||
NamespaceSvc svcnamespaces.Service
|
||||
RepositorySvc svcrepositories.Service
|
||||
TagSvc svctags.Service
|
||||
ArtifactSvc svcartifacts.Service
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
config *config.Configuration
|
||||
auditRepository repoaudit.AuditRepository
|
||||
authorizer authz.Authorizer
|
||||
systemsSvc svcsystems.Service
|
||||
namespaceSvc svcnamespaces.Service
|
||||
repositorySvc svcrepositories.Service
|
||||
tagSvc svctags.Service
|
||||
artifactSvc svcartifacts.Service
|
||||
}
|
||||
|
||||
// Register registers the embedded MCP HTTP endpoint when it is enabled.
|
||||
func Register(digCon *dig.Container) error {
|
||||
return digCon.Invoke(func(params Params) error {
|
||||
if !params.Config.MCP.Enabled {
|
||||
return nil
|
||||
}
|
||||
serverObj := &Server{
|
||||
config: params.Config,
|
||||
auditRepository: params.AuditRepository,
|
||||
authorizer: params.Authorizer,
|
||||
systemsSvc: params.SystemsSvc,
|
||||
namespaceSvc: params.NamespaceSvc,
|
||||
repositorySvc: params.RepositorySvc,
|
||||
tagSvc: params.TagSvc,
|
||||
artifactSvc: params.ArtifactSvc,
|
||||
}
|
||||
handler := serverObj.newHTTPHandler()
|
||||
params.Engine.Any(params.Config.MCP.Path, func(c *gin.Context) {
|
||||
user, ok := utils.GetFromCtx[*models.User](c, consts.ContextUser)
|
||||
if !ok || user == nil || user.Role == enums.UserRoleAnonymous {
|
||||
recordAuthFailure("missing_user")
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeUnauthorized)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(c.GetHeader(consts.HeaderAuthorization), "Basic ") {
|
||||
recordAuthFailure("non_basic_auth")
|
||||
c.Header(consts.HeaderWWWAuthenticate, `Basic realm="sigma-mcp"`)
|
||||
errcode.NewHTTPError(c, errcode.HTTPErrCodeUnauthorized, "MCP requires Basic Auth")
|
||||
return
|
||||
}
|
||||
if params.Config.MCP.MaxRequestBody > 0 && c.Request.Body != nil {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, params.Config.MCP.MaxRequestBody)
|
||||
}
|
||||
ctx := withUser(c.Request.Context(), user)
|
||||
ctx = withClientInfo(ctx, c.ClientIP(), c.Request.UserAgent())
|
||||
handler.ServeHTTP(c.Writer, c.Request.WithContext(ctx))
|
||||
})
|
||||
slog.Info("mcp server registered", "path", params.Config.MCP.Path)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) newHTTPHandler() http.Handler {
|
||||
mcpServer := mcpsdk.NewMCPServer(
|
||||
consts.AppName,
|
||||
version.Version,
|
||||
mcpsdk.WithToolCapabilities(true),
|
||||
mcpsdk.WithResourceCapabilities(true, false),
|
||||
mcpsdk.WithInstructions("Sigma MCP exposes registry management tools. Every request must use Sigma Basic Auth."),
|
||||
)
|
||||
s.registerSystemTools(mcpServer)
|
||||
s.registerNamespaceTools(mcpServer)
|
||||
s.registerRepositoryTools(mcpServer)
|
||||
s.registerTagTools(mcpServer)
|
||||
s.registerArtifactTools(mcpServer)
|
||||
s.registerResources(mcpServer)
|
||||
return mcpsdk.NewStreamableHTTPServer(
|
||||
mcpServer,
|
||||
mcpsdk.WithStateLess(true),
|
||||
mcpsdk.WithHTTPContextFunc(func(ctx context.Context, r *http.Request) context.Context {
|
||||
if user, ok := userFromContext(r.Context()); ok {
|
||||
ctx = withUser(ctx, user)
|
||||
}
|
||||
clientIP, userAgent := clientInfoFromContext(r.Context())
|
||||
return withClientInfo(ctx, clientIP, userAgent)
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
)
|
||||
|
||||
type toolFunc func(context.Context, mcp.CallToolRequest) (any, error)
|
||||
|
||||
func (s *Server) addTool(mcpServer *mcpsdk.MCPServer, name, description string, write bool, handler toolFunc) {
|
||||
tool := mcp.NewTool(name, mcp.WithDescription(description), mcp.WithSchemaAdditionalProperties(true))
|
||||
tool.Annotations = mcp.ToolAnnotation{
|
||||
ReadOnlyHint: new(!write),
|
||||
DestructiveHint: new(write),
|
||||
OpenWorldHint: new(false),
|
||||
}
|
||||
mcpServer.AddTool(tool, s.wrapTool(name, write, handler))
|
||||
}
|
||||
|
||||
func (s *Server) wrapTool(name string, write bool, handler toolFunc) mcpsdk.ToolHandlerFunc {
|
||||
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
startedAt := time.Now()
|
||||
user, ok := userFromContext(ctx)
|
||||
if !ok {
|
||||
recordToolCall(name, toolResultError, startedAt)
|
||||
return mcp.NewToolResultError(errUnauthorized.Error()), nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, s.config.MCP.ToolTimeout)
|
||||
defer cancel()
|
||||
|
||||
data, err := handler(ctx, req)
|
||||
statusCode := statusCodeFromError(err)
|
||||
if write && s.config.MCP.AuditWritesEnabled() {
|
||||
auditToolCall(ctx, s.auditRepository, s.config.MCP.Path, user, name, req, startedAt, statusCode)
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("mcp tool failed", "tool", name, "user_id", user.ID, "err", err)
|
||||
recordToolCall(name, toolResultError, startedAt)
|
||||
return mcp.NewToolResultError(toolErrorMessage(err)), nil
|
||||
}
|
||||
|
||||
recordToolCall(name, toolResultOK, startedAt)
|
||||
return mcp.NewToolResultStructuredOnly(map[string]any{"data": data}), nil
|
||||
}
|
||||
}
|
||||
|
||||
func bindArguments[T any](req mcp.CallToolRequest) (T, error) {
|
||||
var target T
|
||||
if e := req.BindArguments(&target); e != nil {
|
||||
return target, errcode.HTTPErrCodeBadRequest.Detail(e.Error())
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func stringArgument(args map[string]any, names ...string) string {
|
||||
for _, name := range names {
|
||||
if val, ok := args[name].(string); ok {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
)
|
||||
|
||||
func (s *Server) registerArtifactTools(mcpServer *mcpsdk.MCPServer) {
|
||||
s.addTool(mcpServer, "artifact-list", "List repository artifacts", false, s.artifactList)
|
||||
s.addTool(mcpServer, "artifact-get", "Get an artifact by repository name and digest", false, s.artifactGet)
|
||||
s.addTool(mcpServer, "artifact-delete", "Delete an artifact by repository name and digest", true, s.artifactDelete)
|
||||
}
|
||||
|
||||
func (s *Server) artifactList(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.ListArtifactRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repositoryObj, err := s.repositorySvc.GetRepositoryByName(ctx, args.Repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, repositoryObj.ID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
artifacts, total, err := s.artifactSvc.ListArtifacts(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listResponse(artifacts, total, args.Pagination), nil
|
||||
}
|
||||
|
||||
func (s *Server) artifactGet(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.GetArtifactRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repositoryObj, err := s.repositorySvc.GetRepositoryByName(ctx, args.Repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, repositoryObj.ID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return s.artifactSvc.GetArtifact(ctx, args.Repository, args.Digest)
|
||||
}
|
||||
|
||||
func (s *Server) artifactDelete(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.DeleteArtifactRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repositoryObj, err := s.repositorySvc.GetRepositoryByName(ctx, args.Repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, repositoryObj.ID, enums.AuthManage); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.artifactSvc.DeleteArtifact(ctx, args.Repository, args.Digest); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
)
|
||||
|
||||
func (s *Server) registerNamespaceTools(mcpServer *mcpsdk.MCPServer) {
|
||||
s.addTool(mcpServer, "namespace-list", "List namespaces visible to the authenticated user", false, s.namespaceList)
|
||||
s.addTool(mcpServer, "namespace-get", "Get a namespace by id", false, s.namespaceGet)
|
||||
s.addTool(mcpServer, "namespace-create", "Create a namespace", true, s.namespaceCreate)
|
||||
s.addTool(mcpServer, "namespace-update", "Update a namespace", true, s.namespaceUpdate)
|
||||
s.addTool(mcpServer, "namespace-delete", "Delete a namespace", true, s.namespaceDelete)
|
||||
s.addTool(mcpServer, "namespace-member-list", "List namespace members", false, s.namespaceMemberList)
|
||||
s.addTool(mcpServer, "namespace-member-add", "Add a namespace member", true, s.namespaceMemberAdd)
|
||||
s.addTool(mcpServer, "namespace-member-update", "Update a namespace member role", true, s.namespaceMemberUpdate)
|
||||
s.addTool(mcpServer, "namespace-member-delete", "Delete a namespace member", true, s.namespaceMemberDelete)
|
||||
s.addTool(mcpServer, "namespace-member-self-get", "Get authenticated user's namespace membership", false, s.namespaceMemberSelfGet)
|
||||
}
|
||||
|
||||
func (s *Server) namespaceList(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.ListNamespaceRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, total, err := s.namespaceSvc.ListNamespaces(ctx, user.ID, args.Name, args.Pagination, args.Sortable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listResponse(items, total, args.Pagination), nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceGet(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.GetNamespaceRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.ID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
namespaceObj, repositoryCount, tagCount, err := s.namespaceSvc.GetNamespace(ctx, args.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"namespace": namespaceObj,
|
||||
"repository_count": repositoryCount,
|
||||
"tag_count": tagCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceCreate(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.PostNamespaceRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespaceObj, err := s.namespaceSvc.CreateNamespace(ctx, user.ID, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{"id": namespaceObj.ID}, nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceUpdate(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.UpdateNamespaceRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.ID, enums.AuthAdmin); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.namespaceSvc.UpdateNamespace(ctx, user.ID, args.ID, args); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceDelete(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.DeleteNamespaceRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.ID, enums.AuthAdmin); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.namespaceSvc.DeleteNamespace(ctx, user.ID, args.ID); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceMemberList(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.ListNamespaceMemberRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.NamespaceID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
members, total, err := s.namespaceSvc.ListNamespaceMembers(ctx, args.NamespaceID, args.Name, args.Pagination, args.Sortable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listResponse(members, total, args.Pagination), nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceMemberAdd(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.AddNamespaceMemberRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.NamespaceID, enums.AuthAdmin); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
member, err := s.namespaceSvc.AddNamespaceMember(ctx, user.ID, args.NamespaceID, args.UserID, args.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{"id": member.ID}, nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceMemberUpdate(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.UpdateNamespaceMemberRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.NamespaceID, enums.AuthAdmin); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.namespaceSvc.UpdateNamespaceMember(ctx, user.ID, args.NamespaceID, args.UserID, args.Role); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceMemberDelete(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.DeleteNamespaceMemberRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.NamespaceID, enums.AuthAdmin); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.namespaceSvc.DeleteNamespaceMember(ctx, user.ID, args.NamespaceID, args.UserID); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
|
||||
func (s *Server) namespaceMemberSelfGet(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.GetNamespaceMemberSelfRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
member, err := s.namespaceSvc.GetNamespaceMember(ctx, args.NamespaceID, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
)
|
||||
|
||||
func (s *Server) registerRepositoryTools(mcpServer *mcpsdk.MCPServer) {
|
||||
s.addTool(mcpServer, "repository-list", "List repositories in a namespace", false, s.repositoryList)
|
||||
s.addTool(mcpServer, "repository-get", "Get a repository by id", false, s.repositoryGet)
|
||||
s.addTool(mcpServer, "repository-create", "Create a repository", true, s.repositoryCreate)
|
||||
s.addTool(mcpServer, "repository-update", "Update a repository", true, s.repositoryUpdate)
|
||||
s.addTool(mcpServer, "repository-delete", "Delete a repository", true, s.repositoryDelete)
|
||||
}
|
||||
|
||||
func (s *Server) repositoryList(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.ListRepositoryRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.NamespaceID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
repositories, builders, total, err := s.repositorySvc.ListRepositories(
|
||||
ctx, user.ID, args.NamespaceID, args.Name, args.Pagination, args.Sortable,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listResponse(map[string]any{
|
||||
"repositories": repositories,
|
||||
"builders": builders,
|
||||
}, total, args.Pagination), nil
|
||||
}
|
||||
|
||||
func (s *Server) repositoryGet(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.GetRepositoryRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, args.ID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return s.repositorySvc.GetRepository(ctx, args.ID)
|
||||
}
|
||||
|
||||
func (s *Server) repositoryCreate(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.CreateRepositoryRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeNamespace(ctx, user, args.NamespaceID, enums.AuthManage); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
repository, err := s.repositorySvc.CreateRepository(ctx, user.ID, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{"id": repository.ID}, nil
|
||||
}
|
||||
|
||||
func (s *Server) repositoryUpdate(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.UpdateRepositoryRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, args.ID, enums.AuthManage); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.repositorySvc.UpdateRepository(ctx, user.ID, args); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
|
||||
func (s *Server) repositoryDelete(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.DeleteRepositoryRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, args.ID, enums.AuthManage); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.repositorySvc.DeleteRepository(ctx, user.ID, args.NamespaceID, args.ID); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
func (s *Server) registerSystemTools(mcpServer *mcpsdk.MCPServer) {
|
||||
s.addTool(mcpServer, "system-version-get", "Get Sigma build version information", false, s.systemVersionGet)
|
||||
s.addTool(mcpServer, "system-endpoint-get", "Get Sigma public HTTP endpoint", false, s.systemEndpointGet)
|
||||
s.addTool(mcpServer, "system-config-summary-get", "Get a redacted Sigma configuration summary", false, s.systemConfigSummaryGet)
|
||||
}
|
||||
|
||||
func (s *Server) systemVersionGet(ctx context.Context, _ mcp.CallToolRequest) (any, error) {
|
||||
return s.systemsSvc.GetVersion(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) systemEndpointGet(ctx context.Context, _ mcp.CallToolRequest) (any, error) {
|
||||
endpoint, err := s.systemsSvc.GetEndpoint(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{"endpoint": endpoint}, nil
|
||||
}
|
||||
|
||||
func (s *Server) systemConfigSummaryGet(_ context.Context, _ mcp.CallToolRequest) (any, error) {
|
||||
cfg := s.config
|
||||
return map[string]any{
|
||||
"database": map[string]any{
|
||||
"type": cfg.Database.Type,
|
||||
},
|
||||
"storage": map[string]any{
|
||||
"type": cfg.Storage.Type,
|
||||
"redirect": cfg.Storage.Redirect,
|
||||
},
|
||||
"proxy": map[string]any{
|
||||
"enabled": cfg.Proxy.Enabled,
|
||||
},
|
||||
"daemon": map[string]any{
|
||||
"builder": cfg.Daemon.Builder.Enabled,
|
||||
},
|
||||
"audit": map[string]any{
|
||||
"enabled": cfg.Audit.IsEnabled(),
|
||||
"record_list_get": cfg.Audit.RecordListGet,
|
||||
},
|
||||
"mcp": map[string]any{
|
||||
"enabled": cfg.MCP.Enabled,
|
||||
"path": cfg.MCP.Path,
|
||||
"transport": cfg.MCP.Transport,
|
||||
"audit_writes": cfg.MCP.AuditWritesEnabled(),
|
||||
"tool_timeout": cfg.MCP.ToolTimeout.String(),
|
||||
"max_request_body": cfg.MCP.MaxRequestBody,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
mcpsdk "github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
)
|
||||
|
||||
type tagManifestRawGetRequest struct {
|
||||
RepositoryID string `json:"repository_id"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
func (s *Server) registerTagTools(mcpServer *mcpsdk.MCPServer) {
|
||||
s.addTool(mcpServer, "tag-list", "List repository tags", false, s.tagList)
|
||||
s.addTool(mcpServer, "tag-get", "Get a tag by id", false, s.tagGet)
|
||||
s.addTool(mcpServer, "tag-delete", "Delete a tag by id", true, s.tagDelete)
|
||||
s.addTool(mcpServer, "tag-manifest-raw-get", "Get raw manifest content by artifact digest", false, s.tagManifestRawGet)
|
||||
}
|
||||
|
||||
func (s *Server) tagList(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.ListTagRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, args.RepositoryID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
tags, total, err := s.tagSvc.ListTags(ctx, args.NamespaceID, args.RepositoryID, args.Name, args.Type, args.Pagination, args.Sortable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listResponse(tags, total, args.Pagination), nil
|
||||
}
|
||||
|
||||
func (s *Server) tagGet(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.GetTagRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeTag(ctx, user, args.ID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return s.tagSvc.GetTag(ctx, args.ID)
|
||||
}
|
||||
|
||||
func (s *Server) tagDelete(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[api.DeleteTagRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeTag(ctx, user, args.ID, enums.AuthManage); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if e := s.tagSvc.DeleteTag(ctx, args.NamespaceID, args.RepositoryID, args.ID); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return okResponse(), nil
|
||||
}
|
||||
|
||||
func (s *Server) tagManifestRawGet(ctx context.Context, req mcp.CallToolRequest) (any, error) {
|
||||
user, _ := userFromContext(ctx)
|
||||
args, err := bindArguments[tagManifestRawGetRequest](req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e := s.authorizeRepository(ctx, user, args.RepositoryID, enums.AuthRead); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
raw, err := s.tagSvc.GetArtifactRaw(ctx, args.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]string{"raw": string(raw)}, nil
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright 2026 sigma
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mcpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"github.com/go-sigma/sigma/pkg/api"
|
||||
"github.com/go-sigma/sigma/pkg/api/enums"
|
||||
"github.com/go-sigma/sigma/pkg/config"
|
||||
"github.com/go-sigma/sigma/pkg/dal/models"
|
||||
"github.com/go-sigma/sigma/pkg/server/errcode"
|
||||
systemsmocks "github.com/go-sigma/sigma/pkg/service/systems/mocks"
|
||||
)
|
||||
|
||||
func TestWrapTool(t *testing.T) {
|
||||
server := &Server{config: &config.Configuration{MCP: config.ConfigurationMCP{ToolTimeout: time.Second}}}
|
||||
request := mcp.CallToolRequest{}
|
||||
|
||||
t.Run("missing user", func(t *testing.T) {
|
||||
result, err := server.wrapTool("test", false, func(context.Context, mcp.CallToolRequest) (any, error) {
|
||||
t.Fatal("handler should not be called")
|
||||
return nil, nil
|
||||
})(t.Context(), request)
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.IsError)
|
||||
require.Equal(t, "unauthorized", toolResultText(t, result))
|
||||
})
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
ctx := withUser(t.Context(), &models.User{ID: "user-1"})
|
||||
result, err := server.wrapTool("test", false, func(context.Context, mcp.CallToolRequest) (any, error) {
|
||||
return map[string]string{"status": "ok"}, nil
|
||||
})(ctx, request)
|
||||
require.NoError(t, err)
|
||||
require.False(t, result.IsError)
|
||||
require.Equal(t, map[string]any{"data": map[string]string{"status": "ok"}}, result.StructuredContent)
|
||||
})
|
||||
|
||||
t.Run("handler error", func(t *testing.T) {
|
||||
ctx := withUser(t.Context(), &models.User{ID: "user-1"})
|
||||
result, err := server.wrapTool("test", false, func(context.Context, mcp.CallToolRequest) (any, error) {
|
||||
return nil, errcode.HTTPErrCodeBadRequest.Detail("invalid input")
|
||||
})(ctx, request)
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.IsError)
|
||||
require.Equal(t, "invalid input", toolResultText(t, result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestResources(t *testing.T) {
|
||||
resources, err := jsonResource("sigma://test", map[string]string{"status": "ok"})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resources, 1)
|
||||
content, ok := resources[0].(mcp.TextResourceContents)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "sigma://test", content.URI)
|
||||
require.Equal(t, "application/json", content.MIMEType)
|
||||
require.JSONEq(t, `{"status":"ok"}`, content.Text)
|
||||
|
||||
server := &Server{}
|
||||
resources, err = server.capabilitiesResource(t.Context(), mcp.ReadResourceRequest{})
|
||||
require.NoError(t, err)
|
||||
content, ok = resources[0].(mcp.TextResourceContents)
|
||||
require.True(t, ok)
|
||||
require.Contains(t, content.Text, `"namespace-list"`)
|
||||
require.Contains(t, content.Text, `"artifact-delete"`)
|
||||
require.Contains(t, content.Text, `"type":"basic"`)
|
||||
}
|
||||
|
||||
func TestNewHTTPHandler(t *testing.T) {
|
||||
server := &Server{
|
||||
config: &config.Configuration{
|
||||
MCP: config.ConfigurationMCP{ToolTimeout: time.Second},
|
||||
},
|
||||
}
|
||||
|
||||
require.NotNil(t, server.newHTTPHandler())
|
||||
}
|
||||
|
||||
func TestSystemTools(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
service := systemsmocks.NewMockSystemsService(ctrl)
|
||||
service.EXPECT().GetVersion(gomock.Any()).Return(api.GetSystemVersionResponse{Version: "1.0.0"}, nil)
|
||||
service.EXPECT().GetEndpoint(gomock.Any()).Return("https://sigma.example.test", nil)
|
||||
server := &Server{
|
||||
systemsSvc: service,
|
||||
config: &config.Configuration{
|
||||
Database: config.ConfigurationDatabase{Type: enums.DatabasePostgresql},
|
||||
Storage: config.ConfigurationStorage{Type: enums.StorageTypeS3, Redirect: true},
|
||||
Proxy: config.ConfigurationProxy{Enabled: true},
|
||||
MCP: config.ConfigurationMCP{
|
||||
Enabled: true,
|
||||
Path: "/api/v1/mcp",
|
||||
Transport: "streamable_http",
|
||||
ToolTimeout: time.Second,
|
||||
MaxRequestBody: 1024,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
version, err := server.systemVersionGet(t.Context(), mcp.CallToolRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, api.GetSystemVersionResponse{Version: "1.0.0"}, version)
|
||||
|
||||
endpoint, err := server.systemEndpointGet(t.Context(), mcp.CallToolRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]string{"endpoint": "https://sigma.example.test"}, endpoint)
|
||||
|
||||
summary, err := server.systemConfigSummaryGet(t.Context(), mcp.CallToolRequest{})
|
||||
require.NoError(t, err)
|
||||
summaryMap, ok := summary.(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Contains(t, summaryMap, "database")
|
||||
require.Contains(t, summaryMap, "storage")
|
||||
require.Contains(t, summaryMap, "mcp")
|
||||
}
|
||||
|
||||
func TestSystemEndpointGetError(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
service := systemsmocks.NewMockSystemsService(ctrl)
|
||||
expectedErr := errors.New("failed")
|
||||
service.EXPECT().GetEndpoint(gomock.Any()).Return("", expectedErr)
|
||||
|
||||
_, err := (&Server{systemsSvc: service}).systemEndpointGet(t.Context(), mcp.CallToolRequest{})
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
}
|
||||
|
||||
func toolResultText(t *testing.T, result *mcp.CallToolResult) string {
|
||||
t.Helper()
|
||||
require.NotEmpty(t, result.Content)
|
||||
content, ok := mcp.AsTextContent(result.Content[0])
|
||||
require.True(t, ok)
|
||||
return content.Text
|
||||
}
|
||||
@@ -1,17 +1,61 @@
|
||||
---
|
||||
name: "sigma-api-operator"
|
||||
description: "Operates Sigma resources through the current OpenAPI spec. Invoke when external CLI agents need to inspect or manage a Sigma server."
|
||||
description: "Operates Sigma resources through sigma-cli, falling back to the Sigma OpenAPI spec. Invoke when external CLI agents need to inspect or manage a Sigma server."
|
||||
---
|
||||
|
||||
# Sigma API Operator
|
||||
|
||||
Use this skill when an external CLI agent, such as Claude Code, Copilot CLI, or a
|
||||
similar automation tool, needs to inspect or manage Sigma resources through the
|
||||
Sigma HTTP API.
|
||||
similar automation tool, needs to inspect or manage Sigma resources.
|
||||
|
||||
There are two ways to talk to Sigma. Prefer the CLI; use the HTTP API only for
|
||||
resources the CLI does not wrap.
|
||||
|
||||
| Path | Use for |
|
||||
| --- | --- |
|
||||
| `sigma-cli` | namespaces, repositories, tags, artifacts, members, users |
|
||||
| HTTP API + `swagger.yaml` | builders, daemons, webhooks, validators, code repositories, tokens |
|
||||
|
||||
This skill is distributed with `swagger.yaml`, which describes the Sigma REST API
|
||||
surface available to the agent.
|
||||
|
||||
## Preferred: sigma-cli
|
||||
|
||||
Prefer `sigma-cli` whenever it covers the operation. It handles authentication
|
||||
from a credentials file, prints JSON, and keeps the agent away from hand-built
|
||||
URLs.
|
||||
|
||||
```bash
|
||||
sigma-cli login --server "${SIGMA_SERVER}" --username "${SIGMA_USERNAME}" --password "${SIGMA_PASSWORD}"
|
||||
```
|
||||
|
||||
Commands accept IDs (and, for artifacts, names) as positional arguments:
|
||||
|
||||
```bash
|
||||
sigma-cli namespace list --page 1 --limit 10
|
||||
sigma-cli namespace get <namespace_id>
|
||||
sigma-cli namespace create library --visibility public
|
||||
sigma-cli repo list <namespace_id>
|
||||
sigma-cli repo create <namespace_id> library/alpine
|
||||
sigma-cli tag list <namespace_id> <repository_id>
|
||||
sigma-cli artifact list <namespace_name> <repository_name>
|
||||
sigma-cli member add <namespace_id> <user_id> --role reader
|
||||
sigma-cli user list --without-admin
|
||||
```
|
||||
|
||||
Every command prints JSON on success and a formatted error body on failure, so
|
||||
output composes with `jq`:
|
||||
|
||||
```bash
|
||||
sigma-cli namespace list --limit 100 | jq -r '.items[].name'
|
||||
```
|
||||
|
||||
Run `sigma-cli <resource> --help` to confirm the flags of a subcommand before
|
||||
using it. Do not guess flags.
|
||||
|
||||
If a resource is not listed in `sigma-cli --help`, it is not implemented in the
|
||||
CLI yet. Use the HTTP API workflow below instead.
|
||||
|
||||
## API Specification
|
||||
|
||||
Before calling Sigma APIs, load the packaged OpenAPI document:
|
||||
@@ -20,12 +64,6 @@ Before calling Sigma APIs, load the packaged OpenAPI document:
|
||||
swagger.yaml
|
||||
```
|
||||
|
||||
When working from a newer Sigma source checkout, refresh the packaged document:
|
||||
|
||||
```bash
|
||||
go tool swag init --output tools/skill/sigma-api-operator --outputTypes yaml
|
||||
```
|
||||
|
||||
Use the OpenAPI document as the source of truth for:
|
||||
|
||||
- Available paths and HTTP methods.
|
||||
@@ -103,7 +141,7 @@ Common resource prefixes:
|
||||
| Daemons | `/daemons/gc-*` |
|
||||
| Webhooks | `/webhooks/` and `/webhooks/{webhook_id}/logs` |
|
||||
| Validators | `/validators/cron`, `/validators/password`, `/validators/reference`, `/validators/regexp`, `/validators/tag` |
|
||||
| Code repositories | `/coderepos/*` and `/{provider}/repos/coderepos/*` |
|
||||
| Code repositories | `/coderepos/{provider}` |
|
||||
|
||||
## Connection Settings
|
||||
|
||||
@@ -118,7 +156,8 @@ SIGMA_PASSWORD=...
|
||||
Normalize `SIGMA_SERVER` by trimming a trailing slash before building request
|
||||
URLs.
|
||||
|
||||
Sigma management APIs use Basic Auth. Send Basic Auth on each request:
|
||||
`sigma-cli` reads these on its own; for raw HTTP calls, send Basic Auth on each
|
||||
request:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
@@ -129,7 +168,9 @@ curl --fail-with-body --silent --show-error \
|
||||
Do not print passwords, tokens, private keys, access keys, secret keys, or raw
|
||||
Authorization headers.
|
||||
|
||||
## REST Operation Workflow
|
||||
## HTTP Operation Workflow
|
||||
|
||||
Use this only when `sigma-cli` has no matching command.
|
||||
|
||||
1. Read `swagger.yaml`.
|
||||
2. Find the operation that matches the user's resource and action.
|
||||
@@ -156,58 +197,80 @@ Authorization headers.
|
||||
|
||||
## Common Resource Workflows
|
||||
|
||||
Always confirm the exact path, method, and schema in `swagger.yaml` before
|
||||
executing these examples.
|
||||
The resources below are **not** implemented in `sigma-cli`, so call them over
|
||||
HTTP. Always confirm the exact path, method, and schema in `swagger.yaml` before
|
||||
executing an example.
|
||||
|
||||
List namespaces:
|
||||
Garbage collection rules are namespace scoped. Read the current rule:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
"${SIGMA_SERVER}/api/v1/namespaces/?page=1&limit=10"
|
||||
"${SIGMA_SERVER}/api/v1/daemons/gc-artifact/${NAMESPACE_ID}/"
|
||||
```
|
||||
|
||||
Create a namespace:
|
||||
Update the rule, then start a run:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
--header "Content-Type: application/json" \
|
||||
--request POST \
|
||||
--data '{"name":"library","visibility":"public"}' \
|
||||
"${SIGMA_SERVER}/api/v1/namespaces/"
|
||||
```
|
||||
--request PUT \
|
||||
--data '{"cron_rule":"0 2 * * *","cron_enabled":true,"retention_day":7}' \
|
||||
"${SIGMA_SERVER}/api/v1/daemons/gc-artifact/${NAMESPACE_ID}/"
|
||||
|
||||
List repositories in a namespace:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
"${SIGMA_SERVER}/api/v1/namespaces/${NAMESPACE_ID}/repositories/?page=1&limit=10"
|
||||
```
|
||||
|
||||
Create a repository:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
--header "Content-Type: application/json" \
|
||||
--request POST \
|
||||
--data '{"name":"library/alpine","description":"Alpine images"}' \
|
||||
"${SIGMA_SERVER}/api/v1/namespaces/${NAMESPACE_ID}/repositories/"
|
||||
--data '{}' \
|
||||
"${SIGMA_SERVER}/api/v1/daemons/gc-artifact/${NAMESPACE_ID}/runners/"
|
||||
```
|
||||
|
||||
List tags:
|
||||
List webhooks and their delivery logs:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
"${SIGMA_SERVER}/api/v1/namespaces/${NAMESPACE_ID}/tags/?repository_id=${REPOSITORY_ID}&page=1&limit=10"
|
||||
"${SIGMA_SERVER}/api/v1/webhooks/?page=1&limit=10"
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
"${SIGMA_SERVER}/api/v1/webhooks/${WEBHOOK_ID}/logs/?page=1&limit=10"
|
||||
```
|
||||
|
||||
Validate a value before saving it. Validators only check input; they do not read
|
||||
server state, and they return `204 No Content` on success:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Content-Type: application/json" \
|
||||
--request POST \
|
||||
--data '{"cron":"0 2 * * *"}' \
|
||||
"${SIGMA_SERVER}/api/v1/validators/cron"
|
||||
```
|
||||
|
||||
List code repositories for a provider such as `github` or `gitlab`:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
"${SIGMA_SERVER}/api/v1/coderepos/${PROVIDER}?page=1&limit=10"
|
||||
```
|
||||
|
||||
Read the current user's access token metadata:
|
||||
|
||||
```bash
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${SIGMA_USERNAME}:${SIGMA_PASSWORD}" \
|
||||
--header "Accept: application/json" \
|
||||
"${SIGMA_SERVER}/api/v1/tokens"
|
||||
```
|
||||
|
||||
## Safety Rules
|
||||
|
||||
Reference in new issue
Block a user