Skip to content

Commit 39931d7

Browse files
msafarikMarek Safarik
andauthored
MCP server (#7)
feat(MCP-server) This commit establishes the initial MCP server implementation and registers the five tools for Keylime. - Get_all_agents - Get_agent_status - Get_failed_agents - Reactivate_agent - Get_agent_policies --------- Signed-off-by: Marek Safarik <[email protected]> Co-authored-by: Marek Safarik <[email protected]>
1 parent 825c680 commit 39931d7

File tree

17 files changed

+698
-58
lines changed

17 files changed

+698
-58
lines changed

.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Keylime MCP Server Configuration
2+
3+
# Keylime API endpoints
4+
KEYLIME_VERIFIER_URL=https://localhost:8881
5+
KEYLIME_REGISTRAR_URL=https://localhost:8891
6+
KEYLIME_API_VERSION=v2.4
7+
8+
# TLS Configuration
9+
KEYLIME_TLS_ENABLED=true
10+
KEYLIME_IGNORE_HOSTNAME=true
11+
12+
# Certificate paths
13+
# For local development with copied certificates:
14+
KEYLIME_CERT_DIR=/home/YOUR_USERNAME/.keylime/certs
15+
# Or for sudo access to system certificates:
16+
# KEYLIME_CERT_DIR=/var/lib/keylime/cv_ca
17+
18+
# Server configuration
19+
PORT=8080

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ dist/
66
*.out
77

88
# Go
9-
*.exe
10-
*.test
9+
backend/server
1110

1211
# Environment
1312
.env

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ help:
1414
@echo " make logs - View logs"
1515
@echo " make ps - List containers"
1616
@echo " make clean - Remove all"
17+
@echo " make mcp - Build MCP server"
1718

18-
build:
19+
.env:
20+
@if [ ! -f .env ]; then \
21+
cp .env.example .env; \
22+
echo "Created .env from .env.example"; \
23+
fi
24+
25+
build: .env
1926
podman-compose -f compose.yml build
2027

2128
up:
@@ -34,3 +41,6 @@ ps:
3441
clean:
3542
podman-compose -f compose.yml down -v
3643
podman system prune -f
44+
45+
mcp:
46+
cd backend && go build -o server *.go

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,37 @@ This MCP server is a helper tool for working with Keylime. To actually interact
1111
- Network access to the Keylime API endpoints
1212
- [Podman](https://podman.io/getting-started/installation) must be installed on your system.
1313

14-
## Quick Start
14+
## Usage
15+
16+
There are two ways to use this MCP server:
17+
18+
### Option 1: With MCP Client (Claude Desktop, Cline, etc.)
19+
20+
Build the server:
21+
```bash
22+
cd backend
23+
go build -o server *.go
24+
```
25+
26+
You can move the binary anywhere you want (e.g., `/usr/local/bin/server).
27+
28+
Add to your MCP client config (e.g., `~/.config/Claude/claude_desktop_config.json`):
29+
```json
30+
{
31+
"mcpServers": {
32+
"keylime": {
33+
"command": "/full/path/to/keylime-mcp/backend/server",
34+
"args": []
35+
}
36+
}
37+
}
38+
```
39+
40+
**Replace `/full/path/to/keylime-mcp` with your actual path!**
41+
42+
Restart your MCP client. Done.
43+
44+
### Option 2: Web UI (Docker)
1545

1646
```bash
1747
make build
@@ -25,7 +55,7 @@ Run locally without containers:
2555

2656
```bash
2757
# Backend
28-
cd backend && go run main.go
58+
cd backend && go run *.go
2959

3060
# Frontend
3161
cd frontend && pnpm dev

backend/Containerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
FROM golang:1.23-alpine AS builder
22
WORKDIR /app
3-
COPY go.mod ./
3+
COPY go.mod go.sum ./
44
RUN go mod download
55
COPY . .
6-
RUN go build -o server .
6+
RUN go build -o server *.go
77

88
FROM alpine:latest
99
WORKDIR /app

backend/client.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"encoding/json"
8+
"fmt"
9+
"log"
10+
"net/http"
11+
"os"
12+
"strings"
13+
)
14+
15+
// newKeylimeClient creates HTTP client for Keylime API with mTLS support
16+
func newKeylimeClient(baseURL string) *KeylimeClient {
17+
// Remove "http(s)://" prefix if present
18+
baseURL = strings.TrimPrefix(baseURL, "https://")
19+
baseURL = strings.TrimPrefix(baseURL, "http://")
20+
21+
var finalURL string
22+
var httpClient *http.Client
23+
24+
if config.TLSEnabled {
25+
finalURL = "https://" + strings.TrimSuffix(baseURL, "/")
26+
tlsConfig := createTLSConfig()
27+
httpClient = &http.Client{
28+
Transport: &http.Transport{
29+
TLSClientConfig: tlsConfig,
30+
},
31+
}
32+
} else {
33+
finalURL = "http://" + strings.TrimSuffix(baseURL, "/")
34+
httpClient = &http.Client{}
35+
}
36+
37+
return &KeylimeClient{
38+
baseURL: finalURL,
39+
apiVersion: config.APIVersion,
40+
httpClient: httpClient,
41+
}
42+
}
43+
44+
// createTLSConfig creates TLS configuration with mTLS support
45+
// Equivalent to Python's HostNameIgnoreAdapter with SSL context
46+
func createTLSConfig() *tls.Config {
47+
// Load client certificate and key
48+
cert, err := tls.LoadX509KeyPair(config.ClientCert, config.ClientKey)
49+
if err != nil {
50+
log.Printf("Warning: Failed to load client certificate: %v", err)
51+
log.Printf("Attempting to connect without client cert (may fail with mTLS servers)")
52+
// Return basic TLS config without client cert
53+
return &tls.Config{
54+
InsecureSkipVerify: true,
55+
}
56+
}
57+
58+
// Load CA certificate
59+
caCertPEM, err := os.ReadFile(config.CAPath)
60+
if err != nil {
61+
log.Printf("Warning: Failed to load CA certificate: %v", err)
62+
log.Printf("Using system CA pool")
63+
}
64+
65+
// Create CA pool
66+
caCertPool := x509.NewCertPool()
67+
if caCertPEM != nil {
68+
if !caCertPool.AppendCertsFromPEM(caCertPEM) {
69+
log.Printf("Warning: Failed to append CA certificate to pool")
70+
}
71+
}
72+
73+
tlsConfig := &tls.Config{
74+
Certificates: []tls.Certificate{cert},
75+
RootCAs: caCertPool,
76+
// Ignore hostname verification (like Python's HostNameIgnoreAdapter)
77+
// This is needed because Keylime certs often don't have correct hostname
78+
InsecureSkipVerify: config.IgnoreHostname,
79+
}
80+
81+
return tlsConfig
82+
}
83+
84+
func (kc *KeylimeClient) Get(endpoint string) (*http.Response, error) {
85+
url := fmt.Sprintf("%s/%s/%s", kc.baseURL, kc.apiVersion, strings.TrimPrefix(endpoint, "/"))
86+
return kc.httpClient.Get(url)
87+
}
88+
89+
func (kc *KeylimeClient) Post(endpoint string, body interface{}) (*http.Response, error) {
90+
url := fmt.Sprintf("%s/%s/%s", kc.baseURL, kc.apiVersion, strings.TrimPrefix(endpoint, "/"))
91+
var buf bytes.Buffer
92+
if body != nil {
93+
if err := json.NewEncoder(&buf).Encode(body); err != nil {
94+
return nil, fmt.Errorf("failed to marshal body: %w", err)
95+
}
96+
}
97+
req, err := http.NewRequest("POST", url, &buf)
98+
if err != nil {
99+
return nil, err
100+
}
101+
req.Header.Set("Content-Type", "application/json")
102+
return kc.httpClient.Do(req)
103+
}
104+
105+
func (kc *KeylimeClient) Put(endpoint string, body interface{}) (*http.Response, error) {
106+
url := fmt.Sprintf("%s/%s/%s", kc.baseURL, kc.apiVersion, strings.TrimPrefix(endpoint, "/"))
107+
var buf bytes.Buffer
108+
if body != nil {
109+
if err := json.NewEncoder(&buf).Encode(body); err != nil {
110+
return nil, fmt.Errorf("failed to marshal body: %w", err)
111+
}
112+
}
113+
req, err := http.NewRequest("PUT", url, &buf)
114+
if err != nil {
115+
return nil, err
116+
}
117+
req.Header.Set("Content-Type", "application/json")
118+
return kc.httpClient.Do(req)
119+
}
120+
121+
func (kc *KeylimeClient) Delete(endpoint string) (*http.Response, error) {
122+
url := fmt.Sprintf("%s/%s/%s", kc.baseURL, kc.apiVersion, strings.TrimPrefix(endpoint, "/"))
123+
req, err := http.NewRequest("DELETE", url, nil)
124+
if err != nil {
125+
return nil, err
126+
}
127+
return kc.httpClient.Do(req)
128+
}

backend/go.mod

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
module github.com/keylime/keylime-mcp/backend
22

3-
go 1.23
3+
go 1.23.0
44

5+
toolchain go1.24.8
6+
7+
require (
8+
github.com/joho/godotenv v1.5.1
9+
github.com/modelcontextprotocol/go-sdk v1.1.0
10+
)
11+
12+
require (
13+
github.com/google/jsonschema-go v0.3.0 // indirect
14+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
15+
golang.org/x/oauth2 v0.30.0 // indirect
16+
)

backend/go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3+
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
4+
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
5+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
6+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
7+
github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA=
8+
github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
9+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
10+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
11+
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
12+
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
13+
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
14+
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=

backend/handlers.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"log"
6+
"net/http"
7+
)
8+
9+
func healthHandler(w http.ResponseWriter, r *http.Request) {
10+
w.Header().Set("Access-Control-Allow-Origin", "*")
11+
w.Header().Set("Content-Type", "application/json")
12+
13+
response := HealthResponse{
14+
Status: "healthy",
15+
Service: "keylime-mcp-backend",
16+
}
17+
18+
json.NewEncoder(w).Encode(response)
19+
}
20+
21+
func getAllAgentsHandler(w http.ResponseWriter, r *http.Request) {
22+
w.Header().Set("Access-Control-Allow-Origin", "*")
23+
w.Header().Set("Content-Type", "application/json")
24+
25+
resp, err := keylimeRegistrarClient.Get("agents")
26+
if err != nil {
27+
log.Printf("Error fetching agents: %v", err)
28+
http.Error(w, "Failed to fetch agents: "+err.Error(), http.StatusInternalServerError)
29+
return
30+
}
31+
defer resp.Body.Close()
32+
33+
var agents interface{}
34+
err = json.NewDecoder(resp.Body).Decode(&agents)
35+
if err != nil {
36+
log.Printf("Error decoding agents: %v", err)
37+
http.Error(w, "Failed to decode agents response"+err.Error(), http.StatusInternalServerError)
38+
return
39+
}
40+
41+
json.NewEncoder(w).Encode(agents)
42+
}

0 commit comments

Comments
 (0)