From 187f5c4d9765709f798eeb430893e37fc48eeaba Mon Sep 17 00:00:00 2001 From: Tosone Date: Mon, 19 Oct 2026 21:50:21 +0800 Subject: [PATCH] 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 --- .github/workflows/lint.yml | 5 +- AGENTS.md | 2 +- README.md | 9 +- Readme_zh.md | 4 +- conf/config-compose.yaml | 8 - conf/config-dev.yaml | 8 - conf/config-full.yaml | 8 - conf/config.yaml | 9 -- deploy/sigma/templates/configmap.yaml | 7 - deploy/sigma/values.yaml | 7 - docs/.vitepress/config.mts | 8 +- docs/cli.md | 149 +++++++++++++++++++ docs/configuration.md | 8 - docs/mcp.md | 143 ------------------ docs/zh/cli.md | 143 ++++++++++++++++++ docs/zh/configuration.md | 8 - docs/zh/mcp.md | 141 ------------------ go.mod | 4 - go.sum | 8 - pkg/app/helper.go | 6 - pkg/app/server/server.go | 5 - pkg/authz/resolver.go | 9 +- pkg/config/configs_test.go | 28 ---- pkg/config/configuration.go | 42 ------ pkg/logger/logger.go | 10 +- pkg/server/mcp/audit.go | 113 -------------- pkg/server/mcp/audit_test.go | 55 ------- pkg/server/mcp/authorize.go | 55 ------- pkg/server/mcp/authorize_test.go | 128 ---------------- pkg/server/mcp/business_tools_test.go | 154 ------------------- pkg/server/mcp/context.go | 49 ------ pkg/server/mcp/errors.go | 61 -------- pkg/server/mcp/helpers_test.go | 115 -------------- pkg/server/mcp/metrics.go | 64 -------- pkg/server/mcp/resources.go | 102 ------------- pkg/server/mcp/response.go | 29 ---- pkg/server/mcp/server.go | 136 ----------------- pkg/server/mcp/tools.go | 83 ----------- pkg/server/mcp/tools_artifact.go | 86 ----------- pkg/server/mcp/tools_namespace.go | 189 ------------------------ pkg/server/mcp/tools_repository.go | 112 -------------- pkg/server/mcp/tools_system.go | 71 --------- pkg/server/mcp/tools_tag.go | 96 ------------ pkg/server/mcp/tools_test.go | 154 ------------------- tools/skill/sigma-api-operator/SKILL.md | 139 ++++++++++++----- 45 files changed, 418 insertions(+), 2352 deletions(-) create mode 100644 docs/cli.md delete mode 100644 docs/mcp.md create mode 100644 docs/zh/cli.md delete mode 100644 docs/zh/mcp.md delete mode 100644 pkg/server/mcp/audit.go delete mode 100644 pkg/server/mcp/audit_test.go delete mode 100644 pkg/server/mcp/authorize.go delete mode 100644 pkg/server/mcp/authorize_test.go delete mode 100644 pkg/server/mcp/business_tools_test.go delete mode 100644 pkg/server/mcp/context.go delete mode 100644 pkg/server/mcp/errors.go delete mode 100644 pkg/server/mcp/helpers_test.go delete mode 100644 pkg/server/mcp/metrics.go delete mode 100644 pkg/server/mcp/resources.go delete mode 100644 pkg/server/mcp/response.go delete mode 100644 pkg/server/mcp/server.go delete mode 100644 pkg/server/mcp/tools.go delete mode 100644 pkg/server/mcp/tools_artifact.go delete mode 100644 pkg/server/mcp/tools_namespace.go delete mode 100644 pkg/server/mcp/tools_repository.go delete mode 100644 pkg/server/mcp/tools_system.go delete mode 100644 pkg/server/mcp/tools_tag.go delete mode 100644 pkg/server/mcp/tools_test.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 048ecf54..0209a863 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 433afb4d..6a8abca7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/README.md b/README.md index 85e06ce9..763e6700 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Readme_zh.md b/Readme_zh.md index 63e9d4d7..f2204df6 100644 --- a/Readme_zh.md +++ b/Readme_zh.md @@ -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 diff --git a/conf/config-compose.yaml b/conf/config-compose.yaml index 711e3cb8..d63e242b 100644 --- a/conf/config-compose.yaml +++ b/conf/config-compose.yaml @@ -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: diff --git a/conf/config-dev.yaml b/conf/config-dev.yaml index c5cb85d9..90165f4a 100644 --- a/conf/config-dev.yaml +++ b/conf/config-dev.yaml @@ -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 diff --git a/conf/config-full.yaml b/conf/config-full.yaml index a52a31c6..d4030685 100644 --- a/conf/config-full.yaml +++ b/conf/config-full.yaml @@ -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 diff --git a/conf/config.yaml b/conf/config.yaml index 653616a2..b0b2399d 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -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 diff --git a/deploy/sigma/templates/configmap.yaml b/deploy/sigma/templates/configmap.yaml index 6f0a0c18..aa209cbc 100644 --- a/deploy/sigma/templates/configmap.yaml +++ b/deploy/sigma/templates/configmap.yaml @@ -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 diff --git a/deploy/sigma/values.yaml b/deploy/sigma/values.yaml index 83a71d75..2dd46cc7 100644 --- a/deploy/sigma/values.yaml +++ b/deploy/sigma/values.yaml @@ -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: "" diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 54fd0a16..4640ef0d 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -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: { diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..8548a1fe --- /dev/null +++ b/docs/cli.md @@ -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 +sigma-cli namespace create library --visibility public --description "Base images" +sigma-cli namespace update --size-limit 107374182400 +sigma-cli namespace delete +``` + +### Repository + +```bash +sigma-cli repo list +sigma-cli repo get +sigma-cli repo create library/alpine --visibility public +sigma-cli repo update --description "Alpine images" +sigma-cli repo delete +``` + +### Tag + +```bash +sigma-cli tag list --type image +sigma-cli tag get +sigma-cli tag delete +``` + +### Artifact + +Artifact commands take namespace and repository **names** rather than IDs. + +```bash +sigma-cli artifact list +sigma-cli artifact get +sigma-cli artifact delete +``` + +### Member + +```bash +sigma-cli member list +sigma-cli member add --role reader +sigma-cli member update --role admin +sigma-cli member delete +``` + +### 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 --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. diff --git a/docs/configuration.md b/docs/configuration.md index bf9365f8..d9788b24 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 diff --git a/docs/mcp.md b/docs/mcp.md deleted file mode 100644 index fa469543..00000000 --- a/docs/mcp.md +++ /dev/null @@ -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:///api/v1/mcp -``` - -## Authentication - -MCP uses Sigma Basic Auth. Every MCP request must include: - -```text -Authorization: Basic -``` - -For example: - -```bash -printf 'sigma:Admin@123' | base64 -``` - -Use the encoded value in the MCP client configuration: - -```json -{ - "mcpServers": { - "sigma": { - "url": "https:///api/v1/mcp", - "headers": { - "Authorization": "Basic " - } - } - } -} -``` - -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:..." -} -``` diff --git a/docs/zh/cli.md b/docs/zh/cli.md new file mode 100644 index 00000000..4ead5195 --- /dev/null +++ b/docs/zh/cli.md @@ -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 +sigma-cli namespace create library --visibility public --description "基础镜像" +sigma-cli namespace update --size-limit 107374182400 +sigma-cli namespace delete +``` + +### 仓库 + +```bash +sigma-cli repo list +sigma-cli repo get +sigma-cli repo create library/alpine --visibility public +sigma-cli repo update --description "Alpine 镜像" +sigma-cli repo delete +``` + +### Tag + +```bash +sigma-cli tag list --type image +sigma-cli tag get +sigma-cli tag delete +``` + +### 制品 + +制品相关命令使用命名空间和仓库的**名称**,而不是 ID。 + +```bash +sigma-cli artifact list +sigma-cli artifact get +sigma-cli artifact delete +``` + +### 成员 + +```bash +sigma-cli member list +sigma-cli member add --role reader +sigma-cli member update --role admin +sigma-cli member delete +``` + +### 用户 + +```bash +sigma-cli user list --without-admin +sigma-cli user create --username alice --password '...' --email alice@example.com --role user +sigma-cli user update --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 响应体时,先脱敏再展示给用户。 diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 24d0e06d..10f94bd9 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -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 diff --git a/docs/zh/mcp.md b/docs/zh/mcp.md deleted file mode 100644 index ee4149d0..00000000 --- a/docs/zh/mcp.md +++ /dev/null @@ -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:///api/v1/mcp -``` - -## 认证方式 - -MCP 使用 Sigma Basic Auth。每个 MCP 请求都必须携带: - -```text -Authorization: Basic -``` - -例如: - -```bash -printf 'sigma:Admin@123' | base64 -``` - -将编码后的值写入 MCP client 配置: - -```json -{ - "mcpServers": { - "sigma": { - "url": "https:///api/v1/mcp", - "headers": { - "Authorization": "Basic " - } - } - } -} -``` - -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:..." -} -``` diff --git a/go.mod b/go.mod index ea2819b3..dfd653d1 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index b0da007a..24875cc3 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/app/helper.go b/pkg/app/helper.go index ccb336f5..e812581e 100644 --- a/pkg/app/helper.go +++ b/pkg/app/helper.go @@ -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) } } diff --git a/pkg/app/server/server.go b/pkg/app/server/server.go index 7bd38b48..8161d104 100644 --- a/pkg/app/server/server.go +++ b/pkg/app/server/server.go @@ -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, diff --git a/pkg/authz/resolver.go b/pkg/authz/resolver.go index a4a552e1..a67d3d21 100644 --- a/pkg/authz/resolver.go +++ b/pkg/authz/resolver.go @@ -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"): diff --git a/pkg/config/configs_test.go b/pkg/config/configs_test.go index c1696ff3..52153913 100644 --- a/pkg/config/configs_test.go +++ b/pkg/config/configs_test.go @@ -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") diff --git a/pkg/config/configuration.go b/pkg/config/configuration.go index 8207bfd5..20d3a305 100644 --- a/pkg/config/configuration.go +++ b/pkg/config/configuration.go @@ -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"` diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index a8f4e9ef..8db8e588 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -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 { diff --git a/pkg/server/mcp/audit.go b/pkg/server/mcp/audit.go deleted file mode 100644 index c0d52e8a..00000000 --- a/pkg/server/mcp/audit.go +++ /dev/null @@ -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)) -} diff --git a/pkg/server/mcp/audit_test.go b/pkg/server/mcp/audit_test.go deleted file mode 100644 index d1f239b2..00000000 --- a/pkg/server/mcp/audit_test.go +++ /dev/null @@ -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"]) -} diff --git a/pkg/server/mcp/authorize.go b/pkg/server/mcp/authorize.go deleted file mode 100644 index 35d1ed4d..00000000 --- a/pkg/server/mcp/authorize.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/authorize_test.go b/pkg/server/mcp/authorize_test.go deleted file mode 100644 index beb0ed9b..00000000 --- a/pkg/server/mcp/authorize_test.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/business_tools_test.go b/pkg/server/mcp/business_tools_test.go deleted file mode 100644 index a7d50dbf..00000000 --- a/pkg/server/mcp/business_tools_test.go +++ /dev/null @@ -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}, - } -} diff --git a/pkg/server/mcp/context.go b/pkg/server/mcp/context.go deleted file mode 100644 index c7662c1f..00000000 --- a/pkg/server/mcp/context.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/errors.go b/pkg/server/mcp/errors.go deleted file mode 100644 index 04d9029f..00000000 --- a/pkg/server/mcp/errors.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/helpers_test.go b/pkg/server/mcp/helpers_test.go deleted file mode 100644 index 02f8afb7..00000000 --- a/pkg/server/mcp/helpers_test.go +++ /dev/null @@ -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")) -} diff --git a/pkg/server/mcp/metrics.go b/pkg/server/mcp/metrics.go deleted file mode 100644 index e0971b6c..00000000 --- a/pkg/server/mcp/metrics.go +++ /dev/null @@ -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() -} diff --git a/pkg/server/mcp/resources.go b/pkg/server/mcp/resources.go deleted file mode 100644 index 16d82a0b..00000000 --- a/pkg/server/mcp/resources.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/response.go b/pkg/server/mcp/response.go deleted file mode 100644 index 8760330a..00000000 --- a/pkg/server/mcp/response.go +++ /dev/null @@ -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} -} diff --git a/pkg/server/mcp/server.go b/pkg/server/mcp/server.go deleted file mode 100644 index 2129ff71..00000000 --- a/pkg/server/mcp/server.go +++ /dev/null @@ -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) - }), - ) -} diff --git a/pkg/server/mcp/tools.go b/pkg/server/mcp/tools.go deleted file mode 100644 index eeb04d9a..00000000 --- a/pkg/server/mcp/tools.go +++ /dev/null @@ -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 "" -} diff --git a/pkg/server/mcp/tools_artifact.go b/pkg/server/mcp/tools_artifact.go deleted file mode 100644 index 2b7fc030..00000000 --- a/pkg/server/mcp/tools_artifact.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/tools_namespace.go b/pkg/server/mcp/tools_namespace.go deleted file mode 100644 index 1aed280d..00000000 --- a/pkg/server/mcp/tools_namespace.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/tools_repository.go b/pkg/server/mcp/tools_repository.go deleted file mode 100644 index 75d92549..00000000 --- a/pkg/server/mcp/tools_repository.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/tools_system.go b/pkg/server/mcp/tools_system.go deleted file mode 100644 index 305161df..00000000 --- a/pkg/server/mcp/tools_system.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/tools_tag.go b/pkg/server/mcp/tools_tag.go deleted file mode 100644 index 1dd53f87..00000000 --- a/pkg/server/mcp/tools_tag.go +++ /dev/null @@ -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 -} diff --git a/pkg/server/mcp/tools_test.go b/pkg/server/mcp/tools_test.go deleted file mode 100644 index d0191b71..00000000 --- a/pkg/server/mcp/tools_test.go +++ /dev/null @@ -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 -} diff --git a/tools/skill/sigma-api-operator/SKILL.md b/tools/skill/sigma-api-operator/SKILL.md index d58ae8c7..35e70cfc 100644 --- a/tools/skill/sigma-api-operator/SKILL.md +++ b/tools/skill/sigma-api-operator/SKILL.md @@ -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 +sigma-cli namespace create library --visibility public +sigma-cli repo list +sigma-cli repo create library/alpine +sigma-cli tag list +sigma-cli artifact list +sigma-cli member add --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 --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