@agentguard47/mcp-server
Advanced tools
| export {}; |
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { extractDecisionEvents, extractDecisionPayload, isDecisionEvent } from "../decisions.js"; | ||
| test("isDecisionEvent recognizes decision lifecycle events", () => { | ||
| const event = { | ||
| kind: "event", | ||
| name: "decision.approved", | ||
| trace_id: "trace_1", | ||
| data: { | ||
| decision_id: "dec_1", | ||
| workflow_id: "wf_1", | ||
| event_type: "decision.approved", | ||
| }, | ||
| }; | ||
| assert.equal(isDecisionEvent(event), true); | ||
| assert.equal(isDecisionEvent({ kind: "event", name: "tool.result", data: {} }), false); | ||
| }); | ||
| test("extractDecisionPayload normalizes trace and event fields", () => { | ||
| const payload = extractDecisionPayload({ | ||
| kind: "event", | ||
| name: "decision.bound", | ||
| trace_id: "trace_1", | ||
| data: { | ||
| decision_id: "dec_1", | ||
| workflow_id: "wf_1", | ||
| object_type: "deployment", | ||
| object_id: "deploy_1", | ||
| actor_type: "system", | ||
| actor_id: "deploy-api", | ||
| proposal: { action: "deploy" }, | ||
| final: { action: "deploy" }, | ||
| diff: "", | ||
| reason: null, | ||
| comment: null, | ||
| timestamp: "2026-04-07T00:00:00Z", | ||
| binding_state: "applied", | ||
| outcome: "success", | ||
| }, | ||
| }); | ||
| assert.equal(payload?.trace_id, "trace_1"); | ||
| assert.equal(payload?.event_type, "decision.bound"); | ||
| assert.equal(payload?.binding_state, "applied"); | ||
| }); | ||
| test("extractDecisionEvents filters by workflow and trace", () => { | ||
| const decisions = extractDecisionEvents([ | ||
| { | ||
| kind: "event", | ||
| name: "decision.proposed", | ||
| trace_id: "trace_a", | ||
| data: { | ||
| decision_id: "dec_a", | ||
| workflow_id: "wf_a", | ||
| trace_id: "trace_a", | ||
| object_type: "deployment", | ||
| object_id: "deploy_a", | ||
| actor_type: "agent", | ||
| actor_id: "planner", | ||
| event_type: "decision.proposed", | ||
| proposal: { action: "deploy" }, | ||
| final: { action: "deploy" }, | ||
| diff: "", | ||
| reason: null, | ||
| comment: null, | ||
| timestamp: "2026-04-07T00:00:00Z", | ||
| binding_state: null, | ||
| outcome: "proposed", | ||
| }, | ||
| }, | ||
| { | ||
| kind: "event", | ||
| name: "decision.approved", | ||
| trace_id: "trace_b", | ||
| data: { | ||
| decision_id: "dec_b", | ||
| workflow_id: "wf_b", | ||
| trace_id: "trace_b", | ||
| object_type: "ticket", | ||
| object_id: "ticket_b", | ||
| actor_type: "human", | ||
| actor_id: "reviewer", | ||
| event_type: "decision.approved", | ||
| proposal: { action: "close" }, | ||
| final: { action: "close" }, | ||
| diff: "", | ||
| reason: null, | ||
| comment: null, | ||
| timestamp: "2026-04-07T00:01:00Z", | ||
| binding_state: null, | ||
| outcome: "approved", | ||
| }, | ||
| }, | ||
| ], { workflowId: "wf_b", traceId: "trace_b" }); | ||
| assert.equal(decisions.length, 1); | ||
| assert.equal(decisions[0].decision_id, "dec_b"); | ||
| }); |
| export {}; |
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { buildToolShape } from "../schema.js"; | ||
| test("buildToolShape supports string, number, and boolean fields", () => { | ||
| const shape = buildToolShape({ | ||
| trace_id: { type: "string", description: "trace" }, | ||
| limit: { type: "number", description: "limit" }, | ||
| include_errors: { type: "boolean", description: "flag" }, | ||
| }, ["trace_id", "include_errors"]); | ||
| assert.equal(shape.trace_id.safeParse("trace_1").success, true); | ||
| assert.equal(shape.limit.safeParse(5).success, true); | ||
| assert.equal(shape.include_errors.safeParse(true).success, true); | ||
| assert.equal(shape.limit.safeParse(undefined).success, true); | ||
| assert.equal(shape.include_errors.safeParse("yes").success, false); | ||
| }); |
| export {}; |
| import test from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { tools } from "../tools.js"; | ||
| test("get_trace_decisions returns normalized decision payloads", async () => { | ||
| const tool = tools.find((entry) => entry.name === "get_trace_decisions"); | ||
| assert.ok(tool); | ||
| const fakeClient = { | ||
| async getTrace(traceId) { | ||
| return { | ||
| trace_id: traceId, | ||
| events: [ | ||
| { | ||
| kind: "event", | ||
| name: "decision.proposed", | ||
| trace_id: traceId, | ||
| data: { | ||
| decision_id: "dec_1", | ||
| workflow_id: "wf_1", | ||
| trace_id: traceId, | ||
| object_type: "deployment", | ||
| object_id: "deploy_1", | ||
| actor_type: "agent", | ||
| actor_id: "planner", | ||
| event_type: "decision.proposed", | ||
| proposal: { action: "deploy" }, | ||
| final: { action: "deploy" }, | ||
| diff: "", | ||
| reason: null, | ||
| comment: null, | ||
| timestamp: "2026-04-07T00:00:00Z", | ||
| binding_state: null, | ||
| outcome: "proposed", | ||
| }, | ||
| }, | ||
| { | ||
| kind: "event", | ||
| name: "tool.result", | ||
| trace_id: traceId, | ||
| data: { tool_name: "search" }, | ||
| }, | ||
| ], | ||
| }; | ||
| }, | ||
| }; | ||
| const output = await tool.handler(fakeClient, { trace_id: "trace_1" }); | ||
| const parsed = JSON.parse(output); | ||
| assert.equal(parsed.trace_id, "trace_1"); | ||
| assert.equal(parsed.decisions.length, 1); | ||
| assert.equal(parsed.decisions[0].event_type, "decision.proposed"); | ||
| }); |
| declare const DECISION_FIELDS: readonly ["decision_id", "workflow_id", "trace_id", "object_type", "object_id", "actor_type", "actor_id", "event_type", "proposal", "final", "diff", "reason", "comment", "timestamp", "binding_state", "outcome"]; | ||
| export interface TraceEvent { | ||
| kind?: unknown; | ||
| name?: unknown; | ||
| trace_id?: unknown; | ||
| data?: unknown; | ||
| } | ||
| type DecisionField = (typeof DECISION_FIELDS)[number]; | ||
| export type DecisionPayload = Record<DecisionField, unknown>; | ||
| export declare function isDecisionEvent(event: TraceEvent): boolean; | ||
| export declare function extractDecisionPayload(event: TraceEvent): DecisionPayload | null; | ||
| export declare function extractDecisionEvents(events: TraceEvent[], filters?: { | ||
| workflowId?: string; | ||
| decisionId?: string; | ||
| traceId?: string; | ||
| }): DecisionPayload[]; | ||
| export {}; |
| const DECISION_EVENT_TYPES = new Set([ | ||
| "decision.proposed", | ||
| "decision.edited", | ||
| "decision.overridden", | ||
| "decision.approved", | ||
| "decision.bound", | ||
| ]); | ||
| const DECISION_FIELDS = [ | ||
| "decision_id", | ||
| "workflow_id", | ||
| "trace_id", | ||
| "object_type", | ||
| "object_id", | ||
| "actor_type", | ||
| "actor_id", | ||
| "event_type", | ||
| "proposal", | ||
| "final", | ||
| "diff", | ||
| "reason", | ||
| "comment", | ||
| "timestamp", | ||
| "binding_state", | ||
| "outcome", | ||
| ]; | ||
| function isRecord(value) { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| export function isDecisionEvent(event) { | ||
| if (!isRecord(event) || event.kind !== "event") { | ||
| return false; | ||
| } | ||
| if (typeof event.name === "string" && DECISION_EVENT_TYPES.has(event.name)) { | ||
| return isRecord(event.data); | ||
| } | ||
| return isRecord(event.data) && typeof event.data.event_type === "string" && DECISION_EVENT_TYPES.has(event.data.event_type); | ||
| } | ||
| export function extractDecisionPayload(event) { | ||
| if (!isDecisionEvent(event)) { | ||
| return null; | ||
| } | ||
| const payload = isRecord(event.data) ? event.data : {}; | ||
| const normalized = {}; | ||
| for (const field of DECISION_FIELDS) { | ||
| normalized[field] = payload[field]; | ||
| } | ||
| normalized.trace_id = normalized.trace_id ?? event.trace_id; | ||
| normalized.event_type = normalized.event_type ?? event.name; | ||
| return normalized; | ||
| } | ||
| export function extractDecisionEvents(events, filters) { | ||
| return events | ||
| .map((event) => extractDecisionPayload(event)) | ||
| .filter((payload) => payload !== null) | ||
| .filter((payload) => !filters?.workflowId || payload.workflow_id === filters.workflowId) | ||
| .filter((payload) => !filters?.decisionId || payload.decision_id === filters.decisionId) | ||
| .filter((payload) => !filters?.traceId || payload.trace_id === filters.traceId); | ||
| } |
| import { z } from "zod"; | ||
| export declare function buildToolShape(properties: Record<string, unknown>, required?: Iterable<string>): Record<string, z.ZodTypeAny>; |
| import { z } from "zod"; | ||
| export function buildToolShape(properties, required = []) { | ||
| const shape = {}; | ||
| const requiredFields = new Set(required); | ||
| for (const [key, prop] of Object.entries(properties)) { | ||
| const property = prop; | ||
| let field; | ||
| switch (property.type) { | ||
| case "number": | ||
| field = z.number(); | ||
| break; | ||
| case "boolean": | ||
| field = z.boolean(); | ||
| break; | ||
| default: | ||
| field = z.string(); | ||
| break; | ||
| } | ||
| field = field.describe(property.description ?? ""); | ||
| shape[key] = requiredFields.has(key) ? field : field.optional(); | ||
| } | ||
| return shape; | ||
| } |
+3
-16
| #!/usr/bin/env node | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { z } from "zod"; | ||
| import { AgentGuardClient } from "./client.js"; | ||
| import { buildToolShape } from "./schema.js"; | ||
| import { tools } from "./tools.js"; | ||
| const server = new McpServer({ | ||
| name: "agentguard", | ||
| version: "0.2.1", | ||
| version: "0.2.2", | ||
| }); | ||
@@ -21,16 +21,3 @@ let client; | ||
| for (const tool of tools) { | ||
| // Build a Zod schema from the JSON Schema properties | ||
| const shape = {}; | ||
| const required = new Set(tool.inputSchema.required ?? []); | ||
| for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { | ||
| const p = prop; | ||
| let field; | ||
| if (p.type === "number") { | ||
| field = z.number().describe(p.description ?? ""); | ||
| } | ||
| else { | ||
| field = z.string().describe(p.description ?? ""); | ||
| } | ||
| shape[key] = required.has(key) ? field : field.optional(); | ||
| } | ||
| const shape = buildToolShape(tool.inputSchema.properties, tool.inputSchema.required ?? []); | ||
| const toolName = tool.name; | ||
@@ -37,0 +24,0 @@ const handler = tool.handler; |
+22
-0
@@ -0,1 +1,2 @@ | ||
| import { extractDecisionEvents } from "./decisions.js"; | ||
| export const tools = [ | ||
@@ -44,2 +45,23 @@ { | ||
| { | ||
| name: "get_trace_decisions", | ||
| description: "Extract normalized decision.* events from one trace. " + | ||
| "Use this when a workflow includes proposal, override, approval, or binding steps.", | ||
| inputSchema: { | ||
| type: "object", | ||
| properties: { | ||
| trace_id: { type: "string", description: "The trace ID to inspect for decision events" }, | ||
| }, | ||
| required: ["trace_id"], | ||
| }, | ||
| handler: async (client, args) => { | ||
| const traceId = args.trace_id; | ||
| const result = await client.getTrace(traceId); | ||
| const events = Array.isArray(result.events) ? result.events : []; | ||
| const decisions = extractDecisionEvents(events, { | ||
| traceId, | ||
| }); | ||
| return JSON.stringify({ trace_id: traceId, decisions }, null, 2); | ||
| }, | ||
| }, | ||
| { | ||
| name: "get_alerts", | ||
@@ -46,0 +68,0 @@ description: "Get recent guard alerts (loop detection, budget exceeded) and errors. " + |
+8
-5
| { | ||
| "name": "@agentguard47/mcp-server", | ||
| "version": "0.2.1", | ||
| "description": "MCP server for coding-agent traces, alerts, costs, usage, and budget health", | ||
| "version": "0.2.2", | ||
| "description": "Read-only MCP server for coding-agent traces, decision events, alerts, costs, usage, and budget health", | ||
| "mcpName": "io.github.bmdhodl/agentguard47", | ||
@@ -10,3 +10,3 @@ "license": "MIT", | ||
| "type": "git", | ||
| "url": "https://github.com/bmdhodl/agent47.git", | ||
| "url": "git+https://github.com/bmdhodl/agent47.git", | ||
| "directory": "mcp-server" | ||
@@ -24,6 +24,8 @@ }, | ||
| "agent-safety", | ||
| "runtime-guardrails" | ||
| "runtime-guardrails", | ||
| "budget-health", | ||
| "incident-context" | ||
| ], | ||
| "bin": { | ||
| "agentguard-mcp": "./dist/index.js" | ||
| "agentguard-mcp": "dist/index.js" | ||
| }, | ||
@@ -37,2 +39,3 @@ "files": [ | ||
| "start": "node dist/index.js", | ||
| "test": "npm run build && node --test dist/__tests__/*.test.js", | ||
| "prepublishOnly": "npm run build" | ||
@@ -39,0 +42,0 @@ }, |
+33
-6
| # AgentGuard MCP Server | ||
| MCP (Model Context Protocol) server that connects coding agents to the | ||
| AgentGuard Read API. It lets agents inspect their own traces, alerts, usage, | ||
| costs, and saved spend after the local SDK is already in place. | ||
| Read-only MCP (Model Context Protocol) server that connects coding agents to | ||
| the AgentGuard Read API. Use it after the local SDK is already in place and | ||
| you want Codex, Claude Code, Cursor, or another MCP client to inspect traces, | ||
| decision events, alerts, usage, costs, and budget health. | ||
@@ -24,2 +25,3 @@ The boundary is deliberate: | ||
| | `get_trace` | Get the full event tree for a specific trace ID | | ||
| | `get_trace_decisions` | Extract normalized `decision.*` events from a specific trace ID | | ||
| | `get_alerts` | Get guard alerts such as loops, budget exceeded, and errors | | ||
@@ -63,10 +65,33 @@ | `get_usage` | Check event quota usage and plan limits | | ||
| npm run build | ||
| npm test | ||
| npm start | ||
| ``` | ||
| ## Glama / Smithery Build Config | ||
| This repo now includes the files downstream registries expect when they build | ||
| or inspect the MCP server from GitHub: | ||
| - [`Dockerfile`](Dockerfile) - container build for the stdio server | ||
| - [`smithery.yaml`](smithery.yaml) - config schema for `AGENTGUARD_API_KEY` | ||
| and the optional base URL | ||
| That keeps the public repo aligned with the published npm package and makes the | ||
| Glama / Smithery import path explicit instead of implicit. | ||
| The repo root also carries matching shim files for directories that only scan | ||
| the default branch root: | ||
| - [`../Dockerfile`](../Dockerfile) | ||
| - [`../smithery.yaml`](../smithery.yaml) | ||
| Those root files delegate straight to `mcp-server/` and should stay aligned | ||
| with the package-local versions here. | ||
| ## Registry Readiness | ||
| This repo now includes official MCP registry metadata in | ||
| [`server.json`](server.json). The npm package is already public, so the | ||
| remaining registry work is metadata publication: | ||
| [`server.json`](server.json). The npm package is already public, and the Glama / | ||
| Smithery config now lives next to the source, so the remaining registry work is | ||
| metadata publication: | ||
@@ -85,2 +110,4 @@ ```bash | ||
| - `src/client.ts` - HTTP client wrapping `/api/v1/` endpoints | ||
| - `src/tools.ts` - 6 MCP tool definitions and handlers | ||
| - `src/decisions.ts` - Decision-event extraction helpers for hosted traces | ||
| - `src/schema.ts` - JSON Schema to Zod shape builder used during tool registration | ||
| - `src/tools.ts` - 7 MCP tool definitions and handlers |
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
26093
80.18%19
111.11%567
92.2%1
-50%111
32.14%