diff --git a/README.md b/README.md index 8b6c57fe..0e639cc7 100644 --- a/README.md +++ b/README.md @@ -315,62 +315,35 @@ npm install --prefix tools/review-mcp npm install --prefix integrations/tracevault-mcp ``` -Installs `ask_tracevault` — lets agents query indexed session history in natural language ("Why was this refactored?", "What sessions touched the auth service?"). Not required by any policy. Needs `tracevault login` once. +Installs two tools: + +- **`ask_tracevault`** — lets agents query indexed session history in natural language ("Why was this refactored?", "What sessions touched the auth service?"). Not required by any policy. Needs `tracevault login` once. +- **`agent_policies`** — returns the rendered policy instructions for the current repo (same output as `tracevault agent-policies`). Call this at session start so the agent's behaviour matches the configured policies. Claude Code picks up all servers from `.mcp.json` automatically on next session start. No further configuration needed. ## Integrating Visdom Trace into your own project -When you run `tracevault init` in a repository, Visdom Trace installs the session hooks automatically. To get the most out of it — and to make sure your AI agents understand the governance expectations — add a `CLAUDE.md` to your repo with instructions for the agent. +When you run `tracevault init` in a repository, Visdom Trace installs the session hooks automatically. To make sure your AI agents understand the governance expectations, add a `CLAUDE.md` (or equivalent) entry pointing them at `tracevault agent-policies`. ### Recommended `CLAUDE.md` for a Visdom Trace–integrated project ``` # Visdom Trace session tracking -## During work - -Work freely. No special actions needed while coding. - -## Pre-push validation - -When your work is complete and you are ready to push: - -1. Open a validation window — this marks the start of the validation phase: - - tracevault validation-start - -2. Run all required validation tools. Only tools explicitly allowed by - validation-window policies should be called between this point and push. - The exact tools depend on which policies are configured for this repo. - Check the repo's policy list in the Visdom Trace dashboard. - -3. If a validation tool fails and you need to fix the issue, make your fix - and then open a NEW validation window before rerunning the tools: - - tracevault validation-start # invalidates the previous window - - You must rerun ALL expected validation tools after opening a new window. - -4. Once all validation tools have passed, push normally: - - git push +Before starting any session, run `tracevault agent-policies` (or call the +`agent_policies` MCP tool if the tracevault MCP is installed) and follow +the instructions it returns. Those instructions reflect the policies +configured on the server and take precedence over anything below. ## Commit messages Use conventional commits: https://www.conventionalcommits.org/en/v1.0.0/ ``` -> **Note for project maintainers:** The specific tools required in the validation window (e.g. `mcp__review__agent_review`, `mcp__cargo__cargo_fmt`) depend on the policies configured for your repo in the Visdom Trace dashboard. Add those tool names to the "Run all required validation tools" step above when customising this template for your project. - -Adjust the instructions to match your stack. The key behaviors to encode: +`tracevault agent-policies` fetches the active policies from the server and renders Markdown describing which tools must be called, which must succeed, what file patterns trigger conditional checks, and how the validation window works. When policies change in the dashboard, the output updates automatically — you do not need to maintain a manual list in `CLAUDE.md`. -| Instruction | Why | -|---|---| -| Work freely before the validation window | Session-scoped policies can check tools called at any point; Validation-scoped policies only check tools called after `tracevault validation-start` | -| Open validation window when work is done | Marks the start of the pre-push gate phase; only allowed tools should be called between this point and push | -| Restart the window if a fix is needed | `tracevault validation-start` invalidates previous windows; all expected tools must be rerun to satisfy Validation-scoped policies | -| All validation tools must pass before pushing | Policies will block the push if required tools weren't called or didn't succeed | +> **Tool installation:** The rendered instructions reference tools by their literal name (e.g. `mcp__cargo__cargo_fmt`). Make sure those tools are installed in your agent setup. See [Install project-local MCP tools](#4-install-project-local-mcp-tools) for the bundled tools shipped with this repo. ### Policy types you can configure diff --git a/crates/tracevault-cli/src/api_client.rs b/crates/tracevault-cli/src/api_client.rs index f85d92b4..6fd6749e 100644 --- a/crates/tracevault-cli/src/api_client.rs +++ b/crates/tracevault-cli/src/api_client.rs @@ -62,6 +62,23 @@ pub struct CheckResultItem { pub details: String, } +/// Subset of `RepoSettingsResponse` from the server — only the fields we use. +#[derive(Debug, Clone, Deserialize)] +pub struct RepoSettings { + pub validation_window_mode: String, +} + +/// Subset of `PolicyResponse` from the server — only the fields we use for rendering. +#[derive(Debug, Clone, Deserialize)] +pub struct PolicyListItem { + #[allow(dead_code)] + pub name: String, + pub condition: serde_json::Value, + pub action: String, + pub scope: String, + pub enabled: bool, +} + #[derive(Debug, Deserialize)] pub struct RepoListItem { pub id: uuid::Uuid, @@ -272,6 +289,55 @@ impl ApiClient { Ok(repos) } + pub async fn list_policies( + &self, + org_slug: &str, + repo_id: &uuid::Uuid, + ) -> Result, Box> { + let mut builder = self.client.get(format!( + "{}/api/v1/orgs/{}/repos/{}/policies", + self.base_url, org_slug, repo_id + )); + if let Some(key) = &self.api_key { + builder = builder.header("Authorization", format!("Bearer {key}")); + } + + let resp = builder.send().await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(format!("Failed to list policies ({status}): {body}").into()); + } + + let policies: Vec = resp.json().await?; + Ok(policies) + } + + pub async fn get_repo_settings( + &self, + org_slug: &str, + repo_id: &uuid::Uuid, + ) -> Result> { + let mut builder = self.client.get(format!( + "{}/api/v1/orgs/{}/repos/{}/settings", + self.base_url, org_slug, repo_id + )); + if let Some(key) = &self.api_key { + builder = builder.header("Authorization", format!("Bearer {key}")); + } + + let resp = builder.send().await?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + return Err(format!("Failed to fetch repo settings ({status}): {body}").into()); + } + + Ok(resp.json().await?) + } + pub async fn verify_commits( &self, org_slug: &str, diff --git a/crates/tracevault-cli/src/commands/agent_policies.rs b/crates/tracevault-cli/src/commands/agent_policies.rs new file mode 100644 index 00000000..a21de968 --- /dev/null +++ b/crates/tracevault-cli/src/commands/agent_policies.rs @@ -0,0 +1,546 @@ +//! `tracevault agent-policies` — fetch active policies and render +//! agent-readable Markdown instructions. +//! +//! Output is consumed by an agent (Claude Code, Pi, etc.) at session start +//! so its behaviour matches the policies configured on the TraceVault server. + +use crate::api_client::{resolve_credentials, ApiClient, PolicyListItem, RepoSettings}; +use crate::config::TracevaultConfig; +use serde::Deserialize; +use std::path::Path; +use std::process::Command; + +/// Subset of `tracevault_core::policy::PolicyCondition` — only the variants +/// that render into agent instructions. Other variants (TokenBudget, +/// ModelAllowlist, etc.) are server-evaluated without agent action and are +/// intentionally skipped. +#[derive(Debug, Clone, Deserialize)] +#[serde(tag = "type")] +enum Condition { + RequiredToolCall { + tool_names: Vec, + #[serde(default)] + must_succeed: bool, + }, + ConditionalToolCall { + tool_name: String, + #[serde(default)] + when_files_match: Option>, + #[serde(default)] + must_succeed: bool, + }, + /// Variant we don't render — catch-all for unknown / non-tool conditions. + #[serde(other)] + Other, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Action { + Block, + Warn, + Allow, +} + +impl Action { + fn from_str(s: &str) -> Option { + match s { + "block_push" => Some(Action::Block), + "warn" => Some(Action::Warn), + "allow" => Some(Action::Allow), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Scope { + Session, + ValidationWindow, + Both, +} + +impl Scope { + fn from_str(s: &str) -> Option { + match s { + "session" => Some(Scope::Session), + "validation_window" => Some(Scope::ValidationWindow), + "both" => Some(Scope::Both), + _ => None, + } + } + + fn applies_to_session(self) -> bool { + matches!(self, Scope::Session | Scope::Both) + } + + fn applies_to_window(self) -> bool { + matches!(self, Scope::ValidationWindow | Scope::Both) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WindowMode { + Disabled, + Warn, + Block, +} + +impl WindowMode { + fn from_str(s: &str) -> Self { + match s { + "warn" => WindowMode::Warn, + "block" => WindowMode::Block, + _ => WindowMode::Disabled, + } + } +} + +/// One rendered line: a tool requirement under a given scope. +#[derive(Debug, Clone)] +struct ToolRequirement { + tool: String, + action: Action, + must_succeed: bool, + when_files_match: Option>, +} + +impl ToolRequirement { + fn render_line(&self) -> String { + // Allow-listed tools render bare — no "must/should be called" language. + if self.action == Action::Allow { + return format!("- `{}`", self.tool); + } + + let (verb, succeed) = match self.action { + Action::Block => ("must be called", "must succeed"), + Action::Warn => ("should be called", "should succeed"), + Action::Allow => unreachable!(), + }; + + let action_phrase = if self.must_succeed { + format!("{verb} and {succeed}") + } else { + // For unconditional required tools we add "at least once" so it's + // clear duplicates aren't required. + if self.when_files_match.is_some() { + verb.to_string() + } else { + format!("{verb} at least once") + } + }; + + match &self.when_files_match { + Some(patterns) if !patterns.is_empty() => { + let joined = patterns + .iter() + .map(|p| format!("`{p}`")) + .collect::>() + .join(", "); + format!( + "- `{}` — when files matching {joined} are changed, {action_phrase}", + self.tool + ) + } + _ => format!("- `{}` — {action_phrase}", self.tool), + } + } +} + +/// Pure-function renderer. Takes the raw API data and produces the Markdown +/// output. Lives apart from the network layer so it can be unit-tested. +fn render_instructions(policies: &[PolicyListItem], settings: &RepoSettings) -> String { + let window_mode = WindowMode::from_str(&settings.validation_window_mode); + + // Buckets per section. + let mut session_reqs: Vec = Vec::new(); + let mut window_required: Vec = Vec::new(); + let mut window_allowed: Vec = Vec::new(); + + for p in policies.iter().filter(|p| p.enabled) { + let Some(action) = Action::from_str(&p.action) else { + continue; + }; + let Some(scope) = Scope::from_str(&p.scope) else { + continue; + }; + + let cond: Condition = match serde_json::from_value(p.condition.clone()) { + Ok(c) => c, + Err(_) => continue, + }; + + let reqs = condition_to_requirements(&cond, action); + if reqs.is_empty() { + continue; + } + + if scope.applies_to_session() { + session_reqs.extend(reqs.iter().cloned()); + } + if scope.applies_to_window() && window_mode != WindowMode::Disabled { + for r in &reqs { + if r.action == Action::Allow { + window_allowed.push(r.clone()); + } else { + window_required.push(r.clone()); + } + } + } + } + + // No actionable policies at all → terse message. + let has_session_section = !session_reqs.is_empty(); + let has_window_section = (!window_required.is_empty() || !window_allowed.is_empty()) + && window_mode != WindowMode::Disabled; + + if !has_session_section && !has_window_section { + return "## Visdom Trace — agent policy instructions\n\n\ + No active policies for this repository.\n" + .into(); + } + + let mut out = String::new(); + out.push_str("## Visdom Trace — agent policy instructions\n\n"); + out.push_str( + "These instructions reflect the active policies for this repository. \ + They take precedence over any manual instructions elsewhere.\n", + ); + + if has_session_section { + out.push_str("\n### Before push\n"); + out.push_str("The following pre-push checks apply to this repository:\n"); + for r in &session_reqs { + out.push_str(&r.render_line()); + out.push('\n'); + } + } + + if has_window_section { + out.push_str("\n### Validation window (pre-push gating)\n"); + out.push_str( + "A validation window restricts which tools can be called before push, \ + gating the push on a clean validation run. Before pushing, open a \ + validation window:\n\n tracevault validation-start\n\n\ + The window stays open until you push, or until you open a new window. \ + Opening a new window invalidates the prior one.\n", + ); + + if !window_required.is_empty() { + out.push_str("\nRequired tools (must be called inside the window):\n"); + for r in &window_required { + out.push_str(&r.render_line()); + out.push('\n'); + } + } + + if !window_allowed.is_empty() { + out.push_str("\nAllowed tools (may be called freely inside the window):\n"); + for r in &window_allowed { + out.push_str(&r.render_line()); + out.push('\n'); + } + } + + out.push_str( + "\nIf you need to call additional tools after opening the window, open a new window \ + afterwards and rerun all required tools.\n", + ); + } + + out +} + +fn condition_to_requirements(cond: &Condition, action: Action) -> Vec { + match cond { + Condition::RequiredToolCall { + tool_names, + must_succeed, + } => tool_names + .iter() + .map(|t| ToolRequirement { + tool: t.clone(), + action, + must_succeed: *must_succeed, + when_files_match: None, + }) + .collect(), + Condition::ConditionalToolCall { + tool_name, + when_files_match, + must_succeed, + } => vec![ToolRequirement { + tool: tool_name.clone(), + action, + must_succeed: *must_succeed, + when_files_match: when_files_match.clone(), + }], + Condition::Other => Vec::new(), + } +} + +fn git_repo_name(project_root: &Path) -> String { + Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .current_dir(project_root) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .as_deref() + .and_then(|p| p.rsplit('/').next()) + .map(String::from) + .unwrap_or_else(|| "unknown".into()) +} + +pub async fn run(project_root: &Path) -> Result<(), Box> { + let (server_url, token) = resolve_credentials(project_root); + + let server_url = server_url.ok_or("No server URL configured. Run 'tracevault login' first.")?; + let token = token.ok_or("Not logged in. Run 'tracevault login' first.")?; + + let org_slug = TracevaultConfig::load(project_root) + .and_then(|c| c.org_slug) + .ok_or("No org_slug in .tracevault/config.toml. Run 'tracevault init' first.")?; + + let client = ApiClient::new(&server_url, Some(&token)); + + // Resolve repo_id from the local git repo name. + let repo_name = git_repo_name(project_root); + let repos = client.list_repos(&org_slug).await?; + let repo = repos.iter().find(|r| r.name == repo_name).ok_or_else(|| { + format!("Repo '{repo_name}' not found on server. Run 'tracevault sync' first.") + })?; + + let (policies, settings) = tokio::try_join!( + client.list_policies(&org_slug, &repo.id), + client.get_repo_settings(&org_slug, &repo.id), + )?; + + print!("{}", render_instructions(&policies, &settings)); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn settings(mode: &str) -> RepoSettings { + RepoSettings { + validation_window_mode: mode.into(), + } + } + + fn policy( + name: &str, + condition: serde_json::Value, + action: &str, + scope: &str, + enabled: bool, + ) -> PolicyListItem { + PolicyListItem { + name: name.into(), + condition, + action: action.into(), + scope: scope.into(), + enabled, + } + } + + #[test] + fn no_policies_renders_terse_message() { + let out = render_instructions(&[], &settings("disabled")); + assert!(out.contains("No active policies")); + assert!(!out.contains("Before push")); + assert!(!out.contains("Validation window")); + } + + #[test] + fn disabled_policies_are_ignored() { + let p = policy( + "fmt", + json!({"type": "RequiredToolCall", "tool_names": ["cargo_fmt"]}), + "block_push", + "session", + false, // disabled + ); + let out = render_instructions(&[p], &settings("disabled")); + assert!(out.contains("No active policies")); + } + + #[test] + fn session_required_tool_block_renders_must_be_called() { + let p = policy( + "fmt", + json!({"type": "RequiredToolCall", "tool_names": ["cargo_fmt"]}), + "block_push", + "session", + true, + ); + let out = render_instructions(&[p], &settings("disabled")); + assert!(out.contains("### Before push")); + assert!(out.contains("`cargo_fmt`")); + assert!(out.contains("must be called")); + } + + #[test] + fn session_required_tool_warn_renders_should_be_called() { + let p = policy( + "fmt", + json!({"type": "RequiredToolCall", "tool_names": ["cargo_fmt"]}), + "warn", + "session", + true, + ); + let out = render_instructions(&[p], &settings("disabled")); + // The rendered tool line should use "should" language, not "must". + let tool_line = out + .lines() + .find(|l| l.starts_with("- `cargo_fmt`")) + .expect("tool line missing"); + assert!(tool_line.contains("should be called")); + assert!(!tool_line.contains("must be called")); + } + + #[test] + fn must_succeed_modifies_clause() { + let p_block = policy( + "check", + json!({"type": "RequiredToolCall", "tool_names": ["cargo_check"], "must_succeed": true}), + "block_push", + "session", + true, + ); + let out_block = render_instructions(&[p_block], &settings("disabled")); + assert!(out_block.contains("must be called and must succeed")); + + let p_warn = policy( + "check", + json!({"type": "RequiredToolCall", "tool_names": ["cargo_check"], "must_succeed": true}), + "warn", + "session", + true, + ); + let out_warn = render_instructions(&[p_warn], &settings("disabled")); + assert!(out_warn.contains("should be called and should succeed")); + } + + #[test] + fn conditional_tool_call_renders_file_clause() { + let p = policy( + "audit", + json!({ + "type": "ConditionalToolCall", + "tool_name": "cargo_audit", + "when_files_match": ["Cargo.lock"], + "must_succeed": true + }), + "block_push", + "session", + true, + ); + let out = render_instructions(&[p], &settings("disabled")); + assert!(out.contains("when files matching `Cargo.lock` are changed")); + assert!(out.contains("must be called and must succeed")); + } + + #[test] + fn validation_window_required_section_renders() { + let p = policy( + "review", + json!({ + "type": "RequiredToolCall", + "tool_names": ["agent_review"], + "must_succeed": true + }), + "block_push", + "validation_window", + true, + ); + let out = render_instructions(&[p], &settings("block")); + assert!(out.contains("### Validation window")); + assert!(out.contains("Required tools")); + assert!(out.contains("`agent_review`")); + } + + #[test] + fn validation_window_allowed_section_renders() { + let p = policy( + "read-ok", + json!({"type": "RequiredToolCall", "tool_names": ["Read", "Grep"]}), + "allow", + "validation_window", + true, + ); + let out = render_instructions(&[p], &settings("block")); + assert!(out.contains("Allowed tools")); + assert!(out.contains("`Read`")); + assert!(out.contains("`Grep`")); + assert!(!out.contains("must be called")); + } + + #[test] + fn validation_window_section_hidden_when_mode_disabled() { + let p = policy( + "review", + json!({"type": "RequiredToolCall", "tool_names": ["agent_review"]}), + "block_push", + "validation_window", + true, + ); + let out = render_instructions(&[p], &settings("disabled")); + assert!(!out.contains("### Validation window")); + // No session section either since only validation_window-scoped policy → terse output. + assert!(out.contains("No active policies")); + } + + #[test] + fn unknown_condition_type_is_skipped_silently() { + let p = policy( + "budget", + json!({"type": "TokenBudget", "max_tokens": 1000}), + "block_push", + "session", + true, + ); + let out = render_instructions(&[p], &settings("disabled")); + assert!(out.contains("No active policies")); + } + + #[test] + fn scope_both_applies_to_both_sections() { + let p = policy( + "fmt", + json!({"type": "RequiredToolCall", "tool_names": ["cargo_fmt"]}), + "block_push", + "both", + true, + ); + let out = render_instructions(&[p], &settings("block")); + assert!(out.contains("### Before push")); + assert!(out.contains("### Validation window")); + // Tool listed in both sections. + let count = out.matches("`cargo_fmt`").count(); + assert_eq!(count, 2, "expected cargo_fmt in both sections, got: {out}"); + } + + #[test] + fn warn_mode_does_not_advertise_blocking_in_window() { + // The spec says: do NOT emit any "this will block the push" statement + // (we never emit one anyway). Just verify the window section renders + // without making false claims. + let p = policy( + "review", + json!({"type": "RequiredToolCall", "tool_names": ["agent_review"]}), + "warn", + "validation_window", + true, + ); + let out = render_instructions(&[p], &settings("warn")); + assert!(out.contains("### Validation window")); + assert!(!out.contains("will block the push")); + assert!(!out.contains("blocked")); + // Should use 'should be called' (warn action). + assert!(out.contains("should be called")); + } +} diff --git a/crates/tracevault-cli/src/commands/mod.rs b/crates/tracevault-cli/src/commands/mod.rs index ad10d1ac..8a3adbc0 100644 --- a/crates/tracevault-cli/src/commands/mod.rs +++ b/crates/tracevault-cli/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod agent_policies; pub mod check; pub mod commit_push; pub mod flush; diff --git a/crates/tracevault-cli/src/main.rs b/crates/tracevault-cli/src/main.rs index 4fd072a8..57f79da7 100644 --- a/crates/tracevault-cli/src/main.rs +++ b/crates/tracevault-cli/src/main.rs @@ -86,6 +86,15 @@ enum Cli { #[arg(long)] session_id: Option, }, + /// Render agent-readable instructions from active policies. + /// + /// Fetches the active policies for the current repo and outputs Markdown + /// instructions describing which tools the agent must call before pushing, + /// and how validation windows work. Designed to be invoked from CLAUDE.md + /// (or equivalent) at session start so the agent's behaviour matches the + /// policies configured on the TraceVault server. + #[command(name = "agent-policies")] + AgentPolicies, } #[tokio::main] @@ -197,5 +206,12 @@ async fn main() { std::process::exit(1); } } + Cli::AgentPolicies => { + let cwd = env::current_dir().expect("Cannot determine current directory"); + if let Err(e) = commands::agent_policies::run(&cwd).await { + eprintln!("Error: {e}"); + std::process::exit(1); + } + } } } diff --git a/integrations/tracevault-mcp/src/index.ts b/integrations/tracevault-mcp/src/index.ts index 1877dd9c..80ce9382 100644 --- a/integrations/tracevault-mcp/src/index.ts +++ b/integrations/tracevault-mcp/src/index.ts @@ -39,6 +39,10 @@ import { import { readFileSync, existsSync } from "fs"; import { join } from "path"; import { homedir } from "os"; +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); // --------------------------------------------------------------------------- // Config @@ -277,6 +281,46 @@ const TOOL_DESCRIPTION = "'What sessions touched the auth service last month?', " + "'What decisions were made about the database schema?'."; +const AGENT_POLICIES_TOOL = "agent_policies"; +const AGENT_POLICIES_DESCRIPTION = + "Fetch agent-readable Markdown instructions describing the active policies " + + "for the current repository — which tools must be called before push, which " + + "must succeed, which file patterns trigger conditional tool calls, and how " + + "the validation window works. Call this at session start so your behaviour " + + "matches the policies configured on the TraceVault server. The instructions " + + "take precedence over any manual project rules."; + +/** + * Shell out to the installed `tracevault` CLI to render the agent-policies + * instructions. We do this rather than re-implement the rendering in TypeScript + * so there is exactly one rendering implementation to maintain. + */ +async function runAgentPolicies(): Promise { + try { + const { stdout } = await execFileAsync("tracevault", ["agent-policies"], { + // Run from the current working directory so the CLI picks up + // .tracevault/config.toml and resolves the right repo. + cwd: process.cwd(), + maxBuffer: 4 * 1024 * 1024, + }); + return stdout; + } catch (err: unknown) { + const e = err as { code?: string; stderr?: string; message?: string }; + if (e.code === "ENOENT") { + throw new McpError( + ErrorCode.InternalError, + "TraceVault CLI not found on PATH. Install it with `cargo install tracevault-cli` " + + "or follow the project README." + ); + } + const detail = e.stderr?.trim() || e.message || "unknown error"; + throw new McpError( + ErrorCode.InternalError, + `tracevault agent-policies failed: ${detail}` + ); + } +} + async function main(): Promise { let config: Config; try { @@ -309,10 +353,23 @@ async function main(): Promise { required: ["question"], }, }, + { + name: AGENT_POLICIES_TOOL, + description: AGENT_POLICIES_DESCRIPTION, + inputSchema: { + type: "object" as const, + properties: {}, + }, + }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === AGENT_POLICIES_TOOL) { + const text = await runAgentPolicies(); + return { content: [{ type: "text", text }] }; + } + if (request.params.name !== TOOL_NAME) { throw new McpError( ErrorCode.MethodNotFound,