diff --git a/src/frontend/mcp-server/cmd/main.go b/src/frontend/mcp-server/cmd/main.go index 3773fa12..36eec1fa 100644 --- a/src/frontend/mcp-server/cmd/main.go +++ b/src/frontend/mcp-server/cmd/main.go @@ -10,6 +10,23 @@ import ( "mcp-server/internal" ) +// serverInstructions are returned to MCP clients (LLMs like Claude Code) in +// the initialize handshake and are the standard way to steer how the model +// uses the server. The key guidance: the status tools return operational +// snapshots, not incident triggers — a node reporting UNAVAIL or a job in +// PEND/EXIT/UNKWN is normal state. Without this, the model treats every +// non-"OK" value as a problem and floods the user with unsolicited +// diagnostic advice when they only asked to view status. +const serverInstructions = "XCE scheduler agent — cluster status, not an incident console.\n\n" + + "Status tools (scheduler_list_nodes, scheduler_list_jobs, scheduler_get_job, scheduler_cluster_metrics) " + + "return informational snapshots. A node showing UNAVAIL/offline, or a job in PEND/EXIT/UNKWN/TRANSIENT, is " + + "normal operational state, not an error condition.\n\n" + + "When the user asks to view or check status, report the data factually and concisely. Do NOT speculate about " + + "root causes, do NOT reference \"known bugs\", and do NOT propose diagnostic or repair actions (checking logs, " + + "restarting daemons, opening tickets, ssh'ing to nodes, etc.) unless the user explicitly asks for help or " + + "remediation. If you believe action may be warranted, say so in at most one line and let the user decide.\n\n" + + "Write tools (scheduler_submit/hold/release/cancel) are destructive and require the user's explicit intent." + func main() { baseURL := os.Getenv("XCE_API_URL") token := os.Getenv("XCE_API_KEY") @@ -32,6 +49,7 @@ func main() { "0.2.0", mcps.WithResourceCapabilities(true, true), mcps.WithLogging(), + mcps.WithInstructions(serverInstructions), ) // Read-only tools are annotated as such so the MCP client does not prompt @@ -39,7 +57,7 @@ func main() { // ── 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.WithDescription("List jobs in the cluster. Supports filtering by queue, owner, status, and pagination. Status snapshot: report results as-is; job states (PEND/RUN/EXIT/DONE/etc.) are informational, not errors to investigate unprompted."), 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")), @@ -69,7 +87,7 @@ func main() { // ── scheduler_list_nodes ── s.AddTool(mcp.NewTool("scheduler_list_nodes", - mcp.WithDescription("List all compute nodes with their resource usage and status."), + mcp.WithDescription("List all compute nodes with their resource usage and status. Status snapshot: report node states (OK/UNAVAIL/etc.) as-is — a non-OK node is informational, not an error; do not offer diagnostic/repair suggestions unless the user asks."), mcp.WithReadOnlyHintAnnotation(true), mcp.WithDestructiveHintAnnotation(false), ), app.HandleListNodes) @@ -89,7 +107,7 @@ func main() { // ── scheduler_cluster_metrics ── s.AddTool(mcp.NewTool("scheduler_cluster_metrics", - mcp.WithDescription("Get a snapshot of overall cluster resource usage and utilization."), + mcp.WithDescription("Get a snapshot of overall cluster resource usage and utilization. Status snapshot: report as-is; do not speculate about causes or propose actions unless the user asks."), mcp.WithReadOnlyHintAnnotation(true), mcp.WithDestructiveHintAnnotation(false), ), app.HandleClusterMetrics)