mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:41:29 +00:00
fbe053aa0c
Phase 9 ARCH-M2 closure Sprint 10. Splits internal/mcp/tools.go
(was 1867 LOC, the second-largest backend hotspot after the
service/acme.go cuts in Sprints 9 + 9b) via the Option B sibling-
file pattern — new files stay in `package mcp` so every external
caller of `mcp.RegisterTools(...)` resolves the same way. Pure
mechanical relocation; no signature, no behavior, no import-graph
change.
Why this is naturally suited to Option B
========================================
The mcp package already follows the sibling-file convention:
tools_audit_fix.go (registerAuditFixTools), tools_auth.go
(registerAuthTools), tools_auth_bundle2.go (registerAuthBundle2Tools),
and tools_est.go (registerESTTools) each carry a single
register-function each, all in the same `mcp` package. Sprint 10
extends that pattern to the 22 register-functions still inside
tools.go.
The structure of tools.go is unusually clean for a refactor: every
domain has its own `// ── DomainName ──` banner above its
register-function, and every register-function ends with a `}` +
blank line before the next domain's banner. The RegisterTools
dispatcher stayed in tools.go and still invokes each
registerXxxTools(...) in the same order — calls cross a file
boundary but stay in `package mcp`, so same-package resolution
makes them zero-cost.
What moved
==========
New `internal/mcp/tools_certificates.go` (404 LOC) — certificate-
lifecycle domain:
- registerCertificateTools (cert CRUD + revocation)
- registerCRLOCSPTools
- registerRenewalPolicyTools (Phase C P1-1..P1-5)
- registerVerificationTools (Phase G P1-32/P1-34/P1-35)
New `internal/mcp/tools_agents.go` (266 LOC) — agent-management
domain:
- registerAgentTools (per-agent CRUD + lifecycle)
- registerAgentGroupTools
New `internal/mcp/tools_resources.go` (565 LOC) — resource-
management / configuration surface:
- registerIssuerTools, registerTargetTools
- registerPolicyTools, registerProfileTools
- registerTeamTools, registerOwnerTools
- registerNotificationTools
- registerIntermediateCATools (Phase F P1-6..P1-9)
New `internal/mcp/tools_jobs.go` (170 LOC) — workflow domain:
- registerJobTools
- registerApprovalTools + approvalDecisionPayload struct
(Phase A P1-28..P1-31)
New `internal/mcp/tools_discovery.go` (169 LOC) — discovery domain:
- registerNetworkScanTools (Phase D P1-14..P1-19)
- registerDiscoveryReadTools (Phase E P1-10..P1-13)
New `internal/mcp/tools_admin.go` (369 LOC) — observability / admin
domain:
- registerAuditTools, registerStatsTools, registerDigestTools,
registerMetricsTools, registerHealthTools
- registerHealthCheckTools (Phase B P1-20..P1-27)
What stays in tools.go (109 LOC, down from 1867)
================================================
- The RegisterTools dispatcher (still owns the canonical
registration order; calls cross-file but stay in-package).
- The three Bundle-3 wrappers + helper that every register
function consumes: textResult (the json.RawMessage success-path
fence), errorResult (the failure-path fence), paginationQuery
(the URL helper).
The unused `context` import is dropped from tools.go as a clean
side effect — none of the four surviving functions take a
context.Context. Per-import audit on every new file:
- tools_certificates.go: context, fmt, gomcp
- tools_agents.go: context, fmt, net/url, gomcp
- tools_resources.go: context, gomcp
- tools_jobs.go: context, gomcp
- tools_discovery.go: context, gomcp
- tools_admin.go: context, net/url, strconv, gomcp
None of the moved code touched encoding/json directly — that import
stays inside tools.go for textResult's json.RawMessage param.
Bundle-3 fence guardrail update
===============================
The existing TestFenceGuardrail_NoBareCallToolResult guardrail in
fence_guardrail_test.go fails any file that constructs
gomcp.CallToolResult{...} literals outside the tools.go allowlist.
registerCRLOCSPTools — which moved to tools_certificates.go — has
two pre-existing literal CallToolResult constructions: each returns
a server-built status string of the form "DER CRL retrieved (%d
bytes, content-type: %s)" or "OCSP response retrieved (...)". The
byte count is `len(raw)` (server-controlled) and the content-type
comes from the HTTP header on the upstream PKI endpoint
(server-controlled in self-hosted deployments). Both predate
Bundle-3 fencing.
Two options to keep CI green:
(a) Route through textResult — but that changes behavior (adds
the UNTRUSTED MCP_RESPONSE fence around the response), which
breaks the "mechanical relocation, no behavior change" rule
Sprint 10 commits to.
(b) Add tools_certificates.go to the allowlist with a comment
explaining the carve-out is pre-existing and Sprint 10
preserves byte-exact behavior.
This commit takes option (b). The allowlist comment in
fence_guardrail_test.go documents the carve-out, points at the
specific tools (CRL + OCSP binary-pass-through with server-built
status descriptions), and flags tightening these two sites through
textResult as a follow-up concern (open question: does the format
break MCP consumers that parse the description text).
Net effect
==========
tools.go: 1867 → 109 LOC (-1758 = -94.2%). Six new sibling files at
1943 LOC total (109 LOC of header + Phase 9 doc-comment overhead
per file = ~185 LOC of added documentation; the rest is moved
code). The biggest pre-Sprint-10 hotspot in the mcp package is now
smaller than tools_test.go (435 LOC).
Cumulative Phase 9 progress
===========================
config.go 3403 → 1342 (-60.6%, Sprints 1-7)
cmd/server/main.go 2966 → 2260 (-23.8%, Sprints 8 + 8b)
service/acme.go 1965 → 1162 (-40.9%, Sprints 9 + 9b)
mcp/tools.go 1867 → 109 (-94.2%, Sprint 10)
TOTAL across 4 files: 10,201 → 4,873 LOC = -5,328 (-52.2%)
Behavior preservation contract
==============================
1. gofmt -l clean across all 8 affected files.
2. go vet ./internal/mcp/... — no findings.
3. staticcheck ./internal/mcp/... ./cmd/mcp-server/... — no findings.
4. go test -short -count=1 ./internal/mcp/... — green (includes the
TestFenceGuardrail_NoBareCallToolResult guardrail post-allowlist-
update, the tools_per_tool_test.go suite that exercises every
moved register function, and the injection_regression_test.go
suite that pins Bundle-3 fencing behavior on the wrapper layer).
5. Broader-importer build green: go build ./... .
6. Broader-importer tests green: go test -short ./cmd/mcp-server/...
./internal/api/handler/... ./cmd/server/... .
Same-package resolution means the RegisterTools dispatcher's
13-line call list in tools.go reaches each registerXxxTools across
six new sibling files via compile-time-resolved package-level
names; the public mcp.RegisterTools entry point + its (s, client)
signature is unchanged.
What remains for Phase 9
========================
Two sibling-file splits queued:
- Sprint 11: internal/api/handler/auth_session_oidc.go (1577 LOC)
split per handler verb (login / callback / refresh / logout /
backchannel).
- Sprint 12: cmd/agent/main.go (1489 LOC) mirroring the cmd/server
pattern from Sprints 8 + 8b.
Refs: ARCH-M2 (god-files), Phase 9 audit. Sprint 10 closes the MCP
hotspot from the audit's top-6 list.
405 lines
17 KiB
Go
405 lines
17 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
gomcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// Phase 9 ARCH-M2 closure Sprint 10 (2026-05-14): extracted from
|
|
// internal/mcp/tools.go via the Option B sibling-file pattern. Package
|
|
// stays `mcp`; every external caller of RegisterTools(...) resolves
|
|
// the same way — pure mechanical relocation. The dispatcher in
|
|
// tools.go still calls registerCertificateTools / registerCRLOCSPTools
|
|
// / registerRenewalPolicyTools / registerVerificationTools in the
|
|
// same order, just from this file.
|
|
//
|
|
// This file groups the certificate-lifecycle MCP tool domain:
|
|
// certificate CRUD + revocation (registerCertificateTools), CRL/OCSP
|
|
// surface (registerCRLOCSPTools), renewal-policy management
|
|
// (registerRenewalPolicyTools — Phase C of the 2026-05-05 parity
|
|
// audit), and certificate-verification tooling (registerVerificationTools
|
|
// — Phase G P1-32/P1-34/P1-35 of the same audit). Co-locating these
|
|
// four register-functions matches the operator-mental-model boundary
|
|
// (everything a certificate-administrator touches in one file) and
|
|
// pre-dates the Sprint 10 split — tools_audit_fix.go + tools_auth.go +
|
|
// tools_auth_bundle2.go + tools_est.go already follow the same
|
|
// sibling-file convention.
|
|
|
|
// ── Certificates ────────────────────────────────────────────────────
|
|
|
|
func registerCertificateTools(s *gomcp.Server, c *Client) {
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_list_certificates",
|
|
Description: "List managed certificates with optional filters for status, environment, owner, team, and issuer. Returns paginated results.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input ListCertificatesInput) (*gomcp.CallToolResult, any, error) {
|
|
q := paginationQuery(input.Page, input.PerPage)
|
|
if input.Status != "" {
|
|
q.Set("status", input.Status)
|
|
}
|
|
if input.Environment != "" {
|
|
q.Set("environment", input.Environment)
|
|
}
|
|
if input.OwnerID != "" {
|
|
q.Set("owner_id", input.OwnerID)
|
|
}
|
|
if input.TeamID != "" {
|
|
q.Set("team_id", input.TeamID)
|
|
}
|
|
if input.IssuerID != "" {
|
|
q.Set("issuer_id", input.IssuerID)
|
|
}
|
|
data, err := c.Get("/api/v1/certificates", q)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_get_certificate",
|
|
Description: "Get a specific certificate by ID. Returns full certificate details including status, expiry, owner, and tags.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Get("/api/v1/certificates/"+input.ID, nil)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_create_certificate",
|
|
Description: "Create a new managed certificate. Requires name, common_name, renewal_policy_id, issuer_id, owner_id, and team_id.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input CreateCertificateInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Post("/api/v1/certificates", input)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_update_certificate",
|
|
Description: "Update an existing certificate's metadata (name, environment, owner, tags, etc.).",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input UpdateCertificateInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Put("/api/v1/certificates/"+input.ID, input)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_archive_certificate",
|
|
Description: "Archive (soft-delete) a certificate by ID.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Delete("/api/v1/certificates/" + input.ID)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_list_certificate_versions",
|
|
Description: "List all versions (renewals) of a certificate. Shows serial numbers, validity periods, and fingerprints.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input ListVersionsInput) (*gomcp.CallToolResult, any, error) {
|
|
q := paginationQuery(input.Page, input.PerPage)
|
|
data, err := c.Get("/api/v1/certificates/"+input.ID+"/versions", q)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_trigger_renewal",
|
|
Description: "Trigger immediate renewal of a certificate. Creates a renewal job (async, returns 202). Returns 404 if certificate not found, 400 if certificate is archived/expired, 409 if renewal already in progress.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Post("/api/v1/certificates/"+input.ID+"/renew", nil)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_trigger_deployment",
|
|
Description: "Trigger deployment of a certificate to its targets. Optionally specify a single target.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input TriggerDeploymentInput) (*gomcp.CallToolResult, any, error) {
|
|
body := map[string]string{}
|
|
if input.TargetID != "" {
|
|
body["target_id"] = input.TargetID
|
|
}
|
|
data, err := c.Post("/api/v1/certificates/"+input.ID+"/deploy", body)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_revoke_certificate",
|
|
Description: "Revoke a certificate with an optional RFC 5280 reason code. Records in audit trail and notifies the issuer.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input RevokeCertificateInput) (*gomcp.CallToolResult, any, error) {
|
|
body := map[string]string{}
|
|
if input.Reason != "" {
|
|
body["reason"] = input.Reason
|
|
}
|
|
data, err := c.Post("/api/v1/certificates/"+input.ID+"/revoke", body)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_bulk_revoke_certificates",
|
|
Description: "Bulk revoke certificates matching filter criteria. At least one criterion (profile_id, owner_id, agent_id, issuer_id, team_id, or certificate_ids) is required. Returns counts of matched, revoked, skipped, and failed certificates.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input BulkRevokeCertificatesInput) (*gomcp.CallToolResult, any, error) {
|
|
body := map[string]interface{}{
|
|
"reason": input.Reason,
|
|
}
|
|
if input.ProfileID != "" {
|
|
body["profile_id"] = input.ProfileID
|
|
}
|
|
if input.OwnerID != "" {
|
|
body["owner_id"] = input.OwnerID
|
|
}
|
|
if input.AgentID != "" {
|
|
body["agent_id"] = input.AgentID
|
|
}
|
|
if input.IssuerID != "" {
|
|
body["issuer_id"] = input.IssuerID
|
|
}
|
|
if input.TeamID != "" {
|
|
body["team_id"] = input.TeamID
|
|
}
|
|
if len(input.CertificateIDs) > 0 {
|
|
body["certificate_ids"] = input.CertificateIDs
|
|
}
|
|
data, err := c.Post("/api/v1/certificates/bulk-revoke", body)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
// L-1 master closure (cat-l-fa0c1ac07ab5): bulk-renew MCP tool.
|
|
// Mirrors certctl_bulk_revoke_certificates shape sans the Reason
|
|
// field. Server returns total_matched / total_enqueued /
|
|
// total_skipped / total_failed plus per-cert {certificate_id,
|
|
// job_id} pairs in enqueued_jobs.
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_bulk_renew_certificates",
|
|
Description: "Bulk renew certificates matching filter criteria (profile_id, owner_id, agent_id, issuer_id, team_id) or an explicit certificate_ids list. At least one selector required. Returns counts of matched, enqueued, skipped, and failed certificates plus per-cert {certificate_id, job_id} pairs.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input BulkRenewCertificatesInput) (*gomcp.CallToolResult, any, error) {
|
|
body := map[string]interface{}{}
|
|
if input.ProfileID != "" {
|
|
body["profile_id"] = input.ProfileID
|
|
}
|
|
if input.OwnerID != "" {
|
|
body["owner_id"] = input.OwnerID
|
|
}
|
|
if input.AgentID != "" {
|
|
body["agent_id"] = input.AgentID
|
|
}
|
|
if input.IssuerID != "" {
|
|
body["issuer_id"] = input.IssuerID
|
|
}
|
|
if input.TeamID != "" {
|
|
body["team_id"] = input.TeamID
|
|
}
|
|
if len(input.CertificateIDs) > 0 {
|
|
body["certificate_ids"] = input.CertificateIDs
|
|
}
|
|
data, err := c.Post("/api/v1/certificates/bulk-renew", body)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
// L-2 closure (cat-l-8a1fb258a38a): bulk-reassign MCP tool.
|
|
// Narrower than bulk-renew/revoke — IDs-only, no criteria-mode.
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_bulk_reassign_certificates",
|
|
Description: "Bulk reassign owner (and optionally team) for a set of certificates. owner_id is required. team_id is optional and updates only when non-empty. Returns counts of matched, reassigned, skipped (already-owned-by-target), and failed certificates.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input BulkReassignCertificatesInput) (*gomcp.CallToolResult, any, error) {
|
|
body := map[string]interface{}{
|
|
"certificate_ids": input.CertificateIDs,
|
|
"owner_id": input.OwnerID,
|
|
}
|
|
if input.TeamID != "" {
|
|
body["team_id"] = input.TeamID
|
|
}
|
|
data, err := c.Post("/api/v1/certificates/bulk-reassign", body)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
}
|
|
|
|
// ── CRL & OCSP ──────────────────────────────────────────────────────
|
|
//
|
|
// M-006 relocation: CRL and OCSP are served unauthenticated under the
|
|
// RFC 8615 `.well-known/pki/*` namespace (RFC 5280 §5 for CRL, RFC 6960
|
|
// §2.1 for OCSP) so relying parties can retrieve them without a certctl
|
|
// API key. The non-standard JSON CRL tool (`certctl_get_crl`) has been
|
|
// removed — RFC 5280 defines only the DER wire format.
|
|
|
|
func registerCRLOCSPTools(s *gomcp.Server, c *Client) {
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_get_der_crl",
|
|
Description: "Get DER-encoded X.509 CRL for a specific issuer (RFC 5280). Served unauthenticated at /.well-known/pki/crl/{issuer_id}. Returns binary CRL data signed by the issuing CA.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetDERCRLInput) (*gomcp.CallToolResult, any, error) {
|
|
raw, contentType, err := c.GetRaw("/.well-known/pki/crl/" + input.IssuerID)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return &gomcp.CallToolResult{
|
|
Content: []gomcp.Content{
|
|
&gomcp.TextContent{Text: fmt.Sprintf("DER CRL retrieved (%d bytes, content-type: %s)", len(raw), contentType)},
|
|
},
|
|
}, nil, nil
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_ocsp_check",
|
|
Description: "Check OCSP status for a certificate by issuer ID and hex serial number (RFC 6960). Served unauthenticated at /.well-known/pki/ocsp/{issuer_id}/{serial}. Returns good, revoked, or unknown.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input OCSPInput) (*gomcp.CallToolResult, any, error) {
|
|
raw, contentType, err := c.GetRaw("/.well-known/pki/ocsp/" + input.IssuerID + "/" + input.Serial)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return &gomcp.CallToolResult{
|
|
Content: []gomcp.Content{
|
|
&gomcp.TextContent{Text: fmt.Sprintf("OCSP response retrieved (%d bytes, content-type: %s)", len(raw), contentType)},
|
|
},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
// ── Renewal Policies (Phase C — P1-1..P1-5) ─────────────────────────
|
|
//
|
|
// 2026-05-05 CLI/API/MCP↔GUI parity audit closure. The G-1 milestone shipped
|
|
// renewal_policies as a separate resource from the policy engine; the GUI
|
|
// has the page and the API has full CRUD, but MCP previously had zero
|
|
// coverage. Note: the MCP "policy" tools registered by registerPolicyTools
|
|
// already point at /api/v1/renewal-policies (legacy alias) — these new tools
|
|
// expose the renewal-policy domain directly with explicit naming.
|
|
|
|
func registerRenewalPolicyTools(s *gomcp.Server, c *Client) {
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_list_renewal_policies",
|
|
Description: "List renewal policies (GET /api/v1/renewal-policies). Each policy controls renewal-window, retry, and alert-threshold/severity matrix for managed certificates.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input ListParams) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Get("/api/v1/renewal-policies", paginationQuery(input.Page, input.PerPage))
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_get_renewal_policy",
|
|
Description: "Get a single renewal policy (GET /api/v1/renewal-policies/{id}).",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Get("/api/v1/renewal-policies/"+input.ID, nil)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_create_renewal_policy",
|
|
Description: "Create a renewal policy (POST /api/v1/renewal-policies). Required: name. Reasonable defaults exist server-side for renewal_window_days, retries, and alert thresholds.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input CreateRenewalPolicyInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Post("/api/v1/renewal-policies", input)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_update_renewal_policy",
|
|
Description: "Update a renewal policy (PUT /api/v1/renewal-policies/{id}).",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input UpdateRenewalPolicyInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Put("/api/v1/renewal-policies/"+input.ID, input)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_delete_renewal_policy",
|
|
Description: "Delete a renewal policy (DELETE /api/v1/renewal-policies/{id}). Returns HTTP 409 if any managed_certificates still reference the policy (FK-RESTRICT via ErrRenewalPolicyInUse).",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Delete("/api/v1/renewal-policies/" + input.ID)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
}
|
|
|
|
// ── Verification (Phase G — P1-32, P1-34, P1-35) ────────────────────
|
|
//
|
|
// 2026-05-05 CLI/API/MCP↔GUI parity audit closure. P1-33 (POST
|
|
// /api/v1/agents/{id}/discoveries) is intentionally excluded — it is a
|
|
// machine-to-machine push channel for agents reporting filesystem-scan
|
|
// results, not an operator-driven flow. The remaining three round out
|
|
// MCP coverage of certificate-deployment and job-verification surfaces.
|
|
|
|
func registerVerificationTools(s *gomcp.Server, c *Client) {
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_list_certificate_deployments",
|
|
Description: "List deployments for a managed certificate (GET /api/v1/certificates/{id}/deployments). Returns the per-target deployment status rows for the named cert.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Get("/api/v1/certificates/"+input.ID+"/deployments", nil)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_verify_job",
|
|
Description: "Record post-deployment verification for a job (POST /api/v1/jobs/{id}/verify). Required: target_id, expected_fingerprint, actual_fingerprint. Typically called by agents after probing the live TLS endpoint, but exposed here for operator-driven manual verification.",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input VerifyJobInput) (*gomcp.CallToolResult, any, error) {
|
|
body := map[string]any{
|
|
"target_id": input.TargetID,
|
|
"expected_fingerprint": input.ExpectedFingerprint,
|
|
"actual_fingerprint": input.ActualFingerprint,
|
|
"verified": input.Verified,
|
|
}
|
|
if input.Error != "" {
|
|
body["error"] = input.Error
|
|
}
|
|
data, err := c.Post("/api/v1/jobs/"+input.ID+"/verify", body)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
|
|
gomcp.AddTool(s, &gomcp.Tool{
|
|
Name: "certctl_get_job_verification",
|
|
Description: "Get the recorded verification status for a job (GET /api/v1/jobs/{id}/verification). Returns the latest VerificationResult row (expected/actual fingerprint, verified bool, timestamp).",
|
|
}, func(ctx context.Context, req *gomcp.CallToolRequest, input GetByIDInput) (*gomcp.CallToolResult, any, error) {
|
|
data, err := c.Get("/api/v1/jobs/"+input.ID+"/verification", nil)
|
|
if err != nil {
|
|
return errorResult(err)
|
|
}
|
|
return textResult(data)
|
|
})
|
|
}
|