🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@agentguard47/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@agentguard47/mcp-server - npm Package Compare versions

Comparing version
0.2.1
to
0.2.2
+1
dist/__tests__/decisions.test.d.ts
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");
});
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);
});
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;

@@ -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. " +

{
"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 @@ },

# 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