Phase 0: scheduleragent + mcp-server read-only agent framework

Complete the natural-language scheduler agent's Phase 0 read-only pipeline:

Task #2 (API Key auth mechanism):
- New store/apikey.go: APIKey store (hash, validate, expiry)
- controllers/apikey.go: GenerateAPIKey finally persists + registers RBAC role
- controllers/auth.go: GetUsernameFromToken recognizes xce_ak_ prefix
- middleware/validatehandler.go: isValidToken handles API Key
- config/rbac.yaml: mcp_readonly role (view-only permissions)
- 6 new test files covering unit + integration full-chain

Task #3 (schedulerclient Go library):
- types.go: URM types (URMJob/URMQueue/URMNode/JobStatus/Metrics)
- client.go: read-only Client interface (6 methods)
- httpclient.go: resty-based HTTP implementation
- statusmap.go + _test.go: TDD status mapping (33 tests)

Task #4 (mcp-server binary):
- cmd/main.go: entry, reads XCE_API_URL/XCE_API_KEY, registers 6 MCP tools
- internal/app.go: tool handlers wiring schedulerclient to MCP stdio
- Uses github.com/mark3labs/mcp-go@v0.57.0 for stdio transport

All: go vet clean, go build clean, schedulerclient 33/33 tests pass.
mcp-server binary ~11MB arm64, CGO_ENABLED=0.

Architecture:
  Claude Code --stdio--> mcp-server --HTTP--> apiserver REST API --cgo--> C backend

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Lei Chen 2026-07-30 11:16:36 +08:00
parent 98066db68d
commit e4af0cac73
27 changed files with 1760 additions and 7 deletions

View file

@ -0,0 +1,86 @@
package main
import (
"log"
"os"
mcps "github.com/mark3labs/mcp-go/server"
"github.com/mark3labs/mcp-go/mcp"
"mcp-server/internal"
)
func main() {
baseURL := os.Getenv("XCE_API_URL")
token := os.Getenv("XCE_API_KEY")
if baseURL == "" {
baseURL = "https://localhost:8080"
log.Printf("XCE_API_URL not set, defaulting to %s", baseURL)
}
if token == "" {
log.Fatal("XCE_API_KEY environment variable is required")
}
app, err := internal.NewApp(baseURL, token)
if err != nil {
log.Fatalf("failed to initialize: %v", err)
}
s := mcps.NewMCPServer(
"scheduler-agent",
"0.1.0",
mcps.WithResourceCapabilities(true, true),
mcps.WithLogging(),
)
// ── scheduler_list_jobs ──
s.AddTool(mcp.NewTool("scheduler_list_jobs",
mcp.WithDescription("List jobs in the cluster. Supports filtering by queue, owner, status, and pagination."),
mcp.WithString("queue", mcp.Description("Filter by queue name")),
mcp.WithString("owner", mcp.Description("Filter by job owner username")),
mcp.WithString("status", mcp.Description("Filter by status: pending|running|succeeded|failed|cancelled|suspended|unknown")),
mcp.WithNumber("limit", mcp.Description("Max results to return")),
mcp.WithString("page_token", mcp.Description("Pagination token from a prior response")),
), app.HandleListJobs)
// ── scheduler_get_job ──
s.AddTool(mcp.NewTool("scheduler_get_job",
mcp.WithDescription("Get detailed information about a specific job by its ID."),
mcp.WithString("id",
mcp.Description("Job ID"),
mcp.Required(),
),
), app.HandleGetJob)
// ── scheduler_list_queues ──
s.AddTool(mcp.NewTool("scheduler_list_queues",
mcp.WithDescription("List all queues/partitions and their current state (active, inactive, draining)."),
), app.HandleListQueues)
// ── scheduler_list_nodes ──
s.AddTool(mcp.NewTool("scheduler_list_nodes",
mcp.WithDescription("List all compute nodes with their resource usage and status."),
), app.HandleListNodes)
// ── scheduler_job_logs ──
s.AddTool(mcp.NewTool("scheduler_job_logs",
mcp.WithDescription("Retrieve the output logs of a job."),
mcp.WithString("id",
mcp.Description("Job ID"),
mcp.Required(),
),
mcp.WithNumber("tail", mcp.Description("Number of lines from the end to return (0 = all)")),
mcp.WithNumber("offset_bytes", mcp.Description("Byte offset to start reading from")),
), app.HandleJobLogs)
// ── scheduler_cluster_metrics ──
s.AddTool(mcp.NewTool("scheduler_cluster_metrics",
mcp.WithDescription("Get a snapshot of overall cluster resource usage and utilization."),
), app.HandleClusterMetrics)
log.Printf("Starting scheduler-agent MCP server (base-url: %s)", baseURL)
if err := mcps.ServeStdio(s); err != nil {
log.Fatalf("server error: %v", err)
}
}