mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:51:30 +00:00
956230aec1
Separate standalone binary (cmd/mcp-server/) using official MCP Go SDK (modelcontextprotocol/go-sdk v1.4.1) with stdio transport. Stateless HTTP proxy translates MCP tool calls to certctl REST API requests. 76 tools across 16 resource domains with typed input structs and jsonschema tags for automatic LLM-friendly schema generation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
142 lines
3.5 KiB
Go
142 lines
3.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
)
|
|
|
|
// Client is a thin HTTP client that forwards requests to the certctl REST API.
|
|
// It handles auth, base URL resolution, and JSON marshaling.
|
|
type Client struct {
|
|
baseURL string
|
|
apiKey string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewClient creates a new certctl API client.
|
|
func NewClient(baseURL, apiKey string) *Client {
|
|
return &Client{
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Get performs an HTTP GET and returns the raw JSON response body.
|
|
func (c *Client) Get(path string, query url.Values) (json.RawMessage, error) {
|
|
return c.do("GET", path, query, nil)
|
|
}
|
|
|
|
// Post performs an HTTP POST with a JSON body and returns the raw JSON response.
|
|
func (c *Client) Post(path string, body interface{}) (json.RawMessage, error) {
|
|
return c.do("POST", path, nil, body)
|
|
}
|
|
|
|
// Put performs an HTTP PUT with a JSON body and returns the raw JSON response.
|
|
func (c *Client) Put(path string, body interface{}) (json.RawMessage, error) {
|
|
return c.do("PUT", path, nil, body)
|
|
}
|
|
|
|
// Delete performs an HTTP DELETE and returns the raw JSON response (may be empty for 204).
|
|
func (c *Client) Delete(path string) (json.RawMessage, error) {
|
|
return c.do("DELETE", path, nil, nil)
|
|
}
|
|
|
|
// GetRaw performs an HTTP GET and returns the raw response body bytes and content type.
|
|
// Used for binary responses (DER CRL, OCSP).
|
|
func (c *Client) GetRaw(path string) ([]byte, string, error) {
|
|
u, err := url.JoinPath(c.baseURL, path)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("invalid URL: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("creating request: %w", err)
|
|
}
|
|
|
|
if c.apiKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("reading response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, "", fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(data))
|
|
}
|
|
|
|
return data, resp.Header.Get("Content-Type"), nil
|
|
}
|
|
|
|
func (c *Client) do(method, path string, query url.Values, body interface{}) (json.RawMessage, error) {
|
|
u, err := url.JoinPath(c.baseURL, path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid URL: %w", err)
|
|
}
|
|
|
|
if query != nil && len(query) > 0 {
|
|
u = u + "?" + query.Encode()
|
|
}
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshaling request body: %w", err)
|
|
}
|
|
bodyReader = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, u, bodyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
|
}
|
|
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
if c.apiKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading response: %w", err)
|
|
}
|
|
|
|
// 204 No Content — return empty JSON object
|
|
if resp.StatusCode == 204 {
|
|
return json.RawMessage(`{"status":"deleted"}`), nil
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
return json.RawMessage(respBody), nil
|
|
}
|