From 092e8ed3babce630fbcc4421729da72f8de2828a Mon Sep 17 00:00:00 2001 From: Spencer Krum Date: Sun, 14 Jun 2026 11:21:02 -0400 Subject: [PATCH 1/2] docs: simple not on sqlite vs postgres --- website/documentation/1_quickstart.html | 4 ++++ website/md-drafts/1_quickstart.md | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/website/documentation/1_quickstart.html b/website/documentation/1_quickstart.html index 929383a..af77f59 100644 --- a/website/documentation/1_quickstart.html +++ b/website/documentation/1_quickstart.html @@ -46,6 +46,8 @@

Install & initialize Atryum

Download Atryum and register a server in Atryum for testing.

+ +

Download Atryum

curl -fsSL https://github.com/validmind/atryum/raw/main/install_atryum.sh | bash
@@ -56,6 +58,8 @@

Download Atryum

Set up Atryum test server

+ +
  1. Generate a minimal testing configuration with a simple calculator Model Context Protocol (MCP) server that does not require any external credentials:
    ./atryum setup demo
  2. Start the Atryum service and register the test calculator server in Atryum's local database:
    ./atryum run --init-servers
  3. In your browser, navigate to localhost:8080 to open the Atryum local web user interface.

    Here, you can view servers, manage tool invocation approvals, configure rules, and more.

  4. Within Atryum, click Servers in the left sidebar. Confirm that your test calculator server was successfully registered:
    • Verify that the server's connection and auth both display as ready.
    • Verify that Atryum exposed the server at localhost:8080/mcp/calc.
  5. Connect your preferred coding agent to Atryum:
    • Open your agent's MCP settings and add a standard MCP server with the calc server address:
      http://localhost:8080/mcp/calc
    • The agent will think it is talking to a calculator MCP server, but its tool invocations now pass through Atryum first.
  6. Trigger a test tool call from your agent. For example:
    Use the calculator tools and show me 2*2
  7. Within Atryum, click Invocations in the left sidebar to review tool calls. Confirm that the calculator invocation request is Pending Approval — human approval is required by default.
    • Under Approval Required, select Approve to let the tool call run.
    • Verify that the invocation's status is Succeeded and that the approval was decided by a human.
diff --git a/website/md-drafts/1_quickstart.md b/website/md-drafts/1_quickstart.md index 02192fa..a1524c1 100644 --- a/website/md-drafts/1_quickstart.md +++ b/website/md-drafts/1_quickstart.md @@ -6,6 +6,10 @@ Install and initialize Atryum, then integrate Atryum with ValidMind and your oth Download Atryum and register a server in Atryum for testing. +::: +Atryum is not production ready yet. +::: + ### Download Atryum ```bash @@ -18,6 +22,10 @@ The install script downloads the latest Atryum release and selects the correct b ### Set up Atryum test server +::: +This quickstart will run atryum with a local sqlite. This is great for first use, bad for performance and real work. See [docker-compose.yaml](https://github.com/validmind/atryum/blob/main/docker-compose.yml#L2) for an example running with postgres. +::: + 1. Generate a minimal testing configuration with a simple calculator Model Context Protocol (MCP) server that does not require any external credentials: ```bash From d575d45b093235279c556778f256861f21c66954 Mon Sep 17 00:00:00 2001 From: Spencer Krum Date: Sun, 14 Jun 2026 11:33:32 -0400 Subject: [PATCH 2/2] remove: pull atryum_rules_get mcp tool --- internal/api/handlers.go | 91 ++--------------------------------- internal/api/handlers_test.go | 48 +++--------------- 2 files changed, 11 insertions(+), 128 deletions(-) diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 2134a4b..d0051f3 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -608,18 +608,11 @@ func parseAgentIDs(raw string) []string { return out } -const agentRulesToolName = "atryum.rules.get" - // atryumInitializeInstructions is returned in the MCP `initialize` result so -// MCP clients can surface it to the model as system-level guidance. The goal -// is to make the agent aware that approval rules govern tool access and that -// it should consult `atryum.rules.get` before choosing tools. -const atryumInitializeInstructions = "This MCP server (atryum) gates tool calls through approval rules. " + - "Before calling any tool, call the `atryum.rules.get` tool to retrieve the rules that apply to you " + - "and the effective action (`auto_approve`, `human_approval`, or `auto_deny`) for a given server/tool. " + - "Prefer tools that resolve to `auto_approve`. Tools that resolve to `human_approval` will block until a " + - "human responds; tools that resolve to `auto_deny` will be rejected. " + - "Pass the target `server` (or `source`) and `tool` arguments to `atryum.rules.get` to get a disposition." +// MCP clients can surface it to the model as system-level guidance. +const atryumInitializeInstructions = "This MCP server is gated by the Atryum harness. " + + "Atryum may approve, deny, or request human approval for tool calls according to configured rules. " + + "Those rules may change between conversation turns, so treat each tool call as subject to the current Atryum policy." type AgentRule struct { ID string `json:"id"` @@ -640,8 +633,6 @@ type AgentRulesResponse struct { Items []AgentRule `json:"items"` } -var agentRulesToolInputSchema = json.RawMessage(`{"type":"object","properties":{"server":{"type":"string","description":"MCP server/upstream name to evaluate"},"source":{"type":"string","description":"External executor source to evaluate, such as amp"},"tool":{"type":"string","description":"Tool name to evaluate"}},"additionalProperties":false}`) - // ───────────────────────────────────────────────────────────────────────────── type jsonRPCRequest struct { @@ -1130,7 +1121,6 @@ func (h *Handler) handleMCPProxy(w http.ResponseWriter, r *http.Request, server h.writeRPCError(w, req.ID, -32000, err.Error()) return } - tools = appendAtryumTools(tools) _ = h.emitTraceEvent(r.Context(), server, "mcp.tools.list", map[string]any{"tool_count": len(tools), "request_id": requestID}) annotated := h.annotateToolsWithPolicy(r.Context(), server, tools) h.writeRPCResult(w, req.ID, map[string]any{"tools": annotated}) @@ -1143,16 +1133,6 @@ func (h *Handler) handleMCPProxy(w http.ResponseWriter, r *http.Request, server h.writeRPCError(w, req.ID, -32602, "invalid params") return } - if params.Name == agentRulesToolName { - result, err := h.callAgentRulesTool(r.Context(), server, params.Arguments) - if err != nil { - h.writeRPCError(w, req.ID, -32000, err.Error()) - return - } - _ = h.emitTraceEvent(r.Context(), server, "mcp.tools.call", map[string]any{"request_id": requestID, "status": "succeeded", "tool": params.Name}) - h.writeRPCResult(w, req.ID, result) - return - } toolReq := invocation.CreateInvocationRequest{Server: server, Tool: params.Name, Input: params.Arguments} if requestID != "" { toolReq.RequestID = stringPtr(requestID) @@ -1434,10 +1414,6 @@ func (h *Handler) annotateToolsWithPolicy(ctx context.Context, server string, to agentID := auth.AgentIDFromContext(ctx) agentCUID := h.resolveAgentRecordForRules(ctx, agentID) for i, t := range tools { - if t.Name == agentRulesToolName { - out[i] = t - continue - } action, matched := effectiveActionForTool(rules, server, t.Name, agentCUID) desc := t.Description if action != "" { @@ -1475,9 +1451,7 @@ func effectiveActionForTool(rules []store.Rule, server, tool, agentCUID string) } // appendRulesContextToToolResult adds an extra text content block to a denied -// tool call result describing the applicable rules and effective action, so -// the model can learn the policy through feedback instead of having to call -// atryum.rules.get explicitly. +// tool call result describing the applicable rules and effective action. func (h *Handler) appendRulesContextToToolResult(ctx context.Context, result any, server, tool string) any { if h.rulesRepo == nil { return result @@ -1507,61 +1481,6 @@ func (h *Handler) appendRulesContextToToolResult(ctx context.Context, result any return m } -func appendAtryumTools(tools []mcp.Tool) []mcp.Tool { - for _, tool := range tools { - if tool.Name == agentRulesToolName { - return tools - } - } - return append(tools, mcp.Tool{ - Name: agentRulesToolName, - Description: "Return the approval rules and effective action for this agent. Call this before choosing tools so you can prefer auto_approve tools and avoid tools that require human_approval or auto_deny.", - InputSchema: agentRulesToolInputSchema, - }) -} - -func (h *Handler) callAgentRulesTool(ctx context.Context, currentServer string, args map[string]any) (map[string]any, error) { - server := strings.TrimSpace(currentServer) - if server == "" { - server = stringArg(args, "server") - } - if server == "" { - server = stringArg(args, "source") - } - tool := stringArg(args, "tool") - resp, err := h.buildAgentRulesResponse(ctx, auth.AgentIDFromContext(ctx), server, tool) - if err != nil { - return nil, err - } - body, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return nil, err - } - return map[string]any{ - "content": []map[string]any{{ - "type": "text", - "text": string(body), - }}, - "isError": false, - }, nil -} - -func stringArg(args map[string]any, name string) string { - if args == nil { - return "" - } - value, ok := args[name] - if !ok { - return "" - } - switch v := value.(type) { - case string: - return strings.TrimSpace(v) - default: - return strings.TrimSpace(fmt.Sprint(v)) - } -} - func (h *Handler) adminInvocations(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index a022eb6..bad632a 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -399,8 +399,8 @@ func TestMCPInitializeNegotiatesProtocolVersion(t *testing.T) { t.Fatalf("expected protocol version 2025-06-18, got %#v", result["protocolVersion"]) } instructions, ok := result["instructions"].(string) - if !ok || !strings.Contains(instructions, "atryum.rules.get") { - t.Fatalf("expected initialize instructions to mention atryum.rules.get, got %#v", result["instructions"]) + if !ok || !strings.Contains(instructions, "gated by the Atryum harness") || !strings.Contains(instructions, "rules may change between conversation turns") { + t.Fatalf("expected initialize instructions to describe Atryum gating and changing rules, got %#v", result["instructions"]) } } @@ -790,8 +790,8 @@ func TestMCPToolsList(t *testing.T) { if !strings.Contains(w.Body.String(), `"demo_tool"`) { t.Fatalf("expected tools list, got %s", w.Body.String()) } - if !strings.Contains(w.Body.String(), `"atryum.rules.get"`) { - t.Fatalf("expected synthetic rules tool, got %s", w.Body.String()) + if strings.Contains(w.Body.String(), `"atryum.rules.get"`) { + t.Fatalf("did not expect synthetic rules tool, got %s", w.Body.String()) } } @@ -880,41 +880,6 @@ func TestMCPAgentIDQueryHintIgnoredWhenAuthConfigured(t *testing.T) { } } -func TestMCPRulesToolReturnsApplicableRulesWithoutInvocation(t *testing.T) { - rules := &stubRulesRepo{rules: []store.Rule{ - {ID: "read-auto", Action: invocation.RuleActionAutoApprove, ServerPatterns: []string{"demo"}, ToolPatterns: []string{"Read"}, Enabled: true, Order: 0}, - }} - svc := &stubService{} - h := NewHandler(svc, stubServerService{}, nil, rules, nil, nil, nil, nil, nil, nil) - req := httptest.NewRequest(http.MethodPost, "/mcp/demo", strings.NewReader(`{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"atryum.rules.get","arguments":{"tool":"Read"}}}`)) - w := httptest.NewRecorder() - - h.Routes().ServeHTTP(w, req) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String()) - } - if svc.invokedReq != nil { - t.Fatalf("rules tool should not create invocation, got %#v", svc.invokedReq) - } - var rpcResp struct { - Result struct { - Content []struct { - Text string `json:"text"` - } `json:"content"` - } `json:"result"` - } - if err := json.Unmarshal(w.Body.Bytes(), &rpcResp); err != nil { - t.Fatal(err) - } - if len(rpcResp.Result.Content) != 1 { - t.Fatalf("expected one content item, got %#v", rpcResp.Result.Content) - } - if !strings.Contains(rpcResp.Result.Content[0].Text, `"read-auto"`) || !strings.Contains(rpcResp.Result.Content[0].Text, `"action": "auto_approve"`) { - t.Fatalf("expected rules payload in tool result, got %s", rpcResp.Result.Content[0].Text) - } -} - func TestAgentRulesListsApplicableRulesAndDisposition(t *testing.T) { rules := &stubRulesRepo{rules: []store.Rule{ {ID: "bash-deny", Action: invocation.RuleActionAutoDeny, ServerPatterns: []string{"amp"}, ToolPatterns: []string{"Bash"}, Enabled: true, Order: 0}, @@ -1039,9 +1004,8 @@ func TestMCPToolsListAnnotatesEffectiveAction(t *testing.T) { if otherTool.Annotations == nil || otherTool.Annotations.Atryum.EffectiveAction != invocation.RuleActionHumanApproval { t.Fatalf("Other tool annotations: %#v", otherTool.Annotations) } - rulesTool := rpcResp.Result.Tools[byName["atryum.rules.get"]] - if rulesTool.Annotations != nil { - t.Fatalf("synthetic rules tool should not be annotated, got %#v", rulesTool.Annotations) + if _, ok := byName["atryum.rules.get"]; ok { + t.Fatalf("did not expect synthetic rules tool in tools/list") } }