feat(mcp-server): server instructions + report-only status tool guidance

When a user asks to view cluster status (e.g. "看看机器情况"), the LLM saw
non-OK values in the MCP response (a node UNAVAIL, a job PEND/EXIT) and
treated them as incidents — proposing unsolicited diagnostic/repair actions
and referencing "known bugs". The user only wanted a factual status report.

Mitigation at the MCP layer:
- Add server instructions (returned to the client in the initialize
  handshake) stating that the status tools return operational snapshots,
  that UNAVAIL/PEND/EXIT/UNKWN are normal state, and that the model must
  not speculate or propose actions unless the user explicitly asks.
- Append report-only guidance to the scheduler_list_nodes / list_jobs /
  cluster_metrics descriptions.

Verified: initialize response carries the 857-char instructions; tools/list
shows the updated descriptions.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-07 08:43:43 +00:00
parent 057274094d
commit 3db216c628

View file

@ -10,6 +10,23 @@ import (
"mcp-server/internal" "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() { func main() {
baseURL := os.Getenv("XCE_API_URL") baseURL := os.Getenv("XCE_API_URL")
token := os.Getenv("XCE_API_KEY") token := os.Getenv("XCE_API_KEY")
@ -32,6 +49,7 @@ func main() {
"0.2.0", "0.2.0",
mcps.WithResourceCapabilities(true, true), mcps.WithResourceCapabilities(true, true),
mcps.WithLogging(), mcps.WithLogging(),
mcps.WithInstructions(serverInstructions),
) )
// Read-only tools are annotated as such so the MCP client does not prompt // Read-only tools are annotated as such so the MCP client does not prompt
@ -39,7 +57,7 @@ func main() {
// ── scheduler_list_jobs ── // ── scheduler_list_jobs ──
s.AddTool(mcp.NewTool("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("queue", mcp.Description("Filter by queue name")),
mcp.WithString("owner", mcp.Description("Filter by job owner username")), 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.WithString("status", mcp.Description("Filter by status: pending|running|succeeded|failed|cancelled|suspended|unknown")),
@ -69,7 +87,7 @@ func main() {
// ── scheduler_list_nodes ── // ── scheduler_list_nodes ──
s.AddTool(mcp.NewTool("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.WithReadOnlyHintAnnotation(true),
mcp.WithDestructiveHintAnnotation(false), mcp.WithDestructiveHintAnnotation(false),
), app.HandleListNodes) ), app.HandleListNodes)
@ -89,7 +107,7 @@ func main() {
// ── scheduler_cluster_metrics ── // ── scheduler_cluster_metrics ──
s.AddTool(mcp.NewTool("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.WithReadOnlyHintAnnotation(true),
mcp.WithDestructiveHintAnnotation(false), mcp.WithDestructiveHintAnnotation(false),
), app.HandleClusterMetrics) ), app.HandleClusterMetrics)