Skip to main content
Glama

Architectural decision log

decisions_log

Record, list, and search architectural decisions. Store decisions with title, body, category, importance; supersede old ones. List recent decisions or search via full-text query. Helps AI agents retain project context across sessions.

Instructions

Multi-action ADR log. Pick one via action: • store — record a new decision (title + body required; supersedes_id optional). Writes a row. • list — list recent decisions (read-only). • search — FTS over decisions (query required, read-only). Decisions outrank free-form memories by default and survive pruning longer. Use pitfalls_log for recurring problems.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
actionYesWhich sub-operation to perform: `store`, `list`, or `search`.
project_pathYesAbsolute project path the decision belongs to (or to scope `list`/`search`). Required.
titleNoDecision title (required for `store`, ignored otherwise). One short line — e.g. `"Use Postgres over MySQL"`.
bodyNoDecision body in markdown (required for `store`). Should explain context, options considered, and rationale.
categoryNoFree-form category tag for filtering, e.g. `architecture`, `infra`, `process`. Default `general`.general
importanceNoImportance score in [0, 1] for `store`. Default 0.7 — decisions outrank typical facts (0.5).
supersedes_idNoFor `store`: optional id of an earlier decision this one replaces. The older decision is marked superseded.
queryNoSearch text (required for `action="search"`). Tokenised by FTS5.
limitNoMaximum rows to return for `list`/`search` (1-50). Default 10.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
messageYesFor `store`: `Decision stored with ID: <id>`. For `list`/`search`: markdown bullet list of decisions or `No decisions found.` Returns `Invalid action: ...` when `action` is unsupported.

Implementation Reference

  • Main handler function for the 'decisions_log' tool. Dispatches to 'store', 'list', or 'search' actions on the DecisionsRepo.
    export async function handleDecisionsLog(repo: DecisionsRepo, params: {
      action: string; project_path: string; title?: string; body?: string;
      category?: string; importance?: number; supersedes_id?: string;
      query?: string; limit?: number;
    }): Promise<string> {
      if (params.action === "store") {
        if (!params.title || !params.body) return "title and body are required for action='store'.";
        const id = repo.store(params.project_path, params.title, params.body, params.category, params.importance, params.supersedes_id);
        return `Decision stored with ID: ${id}`;
      }
      if (params.action === "list") {
        const decisions = repo.list(params.project_path, params.limit);
        if (!decisions.length) return "No decisions found.";
        return decisions.map(d =>
          `[${d.category}] ${d.title}\n  ID: ${d.id}\n  ${(d.body as string).slice(0, 300)}\n  Importance: ${d.importance_score} | Created: ${d.created_at}`
        ).join("\n\n");
      }
      if (params.action === "search") {
        if (!params.query) return "query is required for action='search'.";
        const results = repo.search(params.query, params.project_path, params.limit);
        if (!results.length) return "No decisions found.";
        return results.map(d =>
          `[${d.category}] ${d.title}\n  ID: ${d.id}\n  ${(d.body as string).slice(0, 300)}`
        ).join("\n\n");
      }
      return `Invalid action: ${params.action}. Use 'store', 'list', or 'search'.`;
    }
  • Input schema for decisions_log: defines action, project_path, title, body, category, importance, supersedes_id, query, and limit parameters with Zod validation.
    inputSchema: {
      action: z.enum(["store", "list", "search"]).describe("Which sub-operation to perform: `store`, `list`, or `search`."),
      project_path: z.string().describe("Absolute project path the decision belongs to (or to scope `list`/`search`). Required."),
      title: z.string().default("").describe("Decision title (required for `store`, ignored otherwise). One short line — e.g. `\"Use Postgres over MySQL\"`."),
      body: z.string().default("").describe("Decision body in markdown (required for `store`). Should explain context, options considered, and rationale."),
      category: z.string().default("general").describe("Free-form category tag for filtering, e.g. `architecture`, `infra`, `process`. Default `general`."),
      importance: z.number().min(0).max(1).default(0.7).describe("Importance score in [0, 1] for `store`. Default 0.7 — decisions outrank typical facts (0.5)."),
      supersedes_id: z.string().default("").describe("For `store`: optional id of an earlier decision this one replaces. The older decision is marked superseded."),
      query: z.string().default("").describe("Search text (required for `action=\"search\"`). Tokenised by FTS5."),
      limit: z.number().int().min(1).max(50).default(10).describe("Maximum rows to return for `list`/`search` (1-50). Default 10."),
    },
  • Output schema for decisions_log: returns a message string describing the result.
    outputSchema: {
      message: z.string().describe("For `store`: `Decision stored with ID: <id>`. For `list`/`search`: markdown bullet list of decisions or `No decisions found.` Returns `Invalid action: ...` when `action` is unsupported."),
    },
  • src/index.ts:439-473 (registration)
    Registration of the 'decisions_log' tool on the server with title, description, input/output schemas, annotations, and handler binding.
    server.registerTool(
      "decisions_log",
      {
        title: "Architectural decision log",
        description: [
          "Multi-action ADR log. Pick one via `action`:",
          "  • `store` — record a new decision (`title` + `body` required; `supersedes_id` optional). Writes a row.",
          "  • `list` — list recent decisions (read-only).",
          "  • `search` — FTS over decisions (`query` required, read-only).",
          "Decisions outrank free-form memories by default and survive pruning longer. Use `pitfalls_log` for recurring problems.",
        ].join(" "),
        inputSchema: {
          action: z.enum(["store", "list", "search"]).describe("Which sub-operation to perform: `store`, `list`, or `search`."),
          project_path: z.string().describe("Absolute project path the decision belongs to (or to scope `list`/`search`). Required."),
          title: z.string().default("").describe("Decision title (required for `store`, ignored otherwise). One short line — e.g. `\"Use Postgres over MySQL\"`."),
          body: z.string().default("").describe("Decision body in markdown (required for `store`). Should explain context, options considered, and rationale."),
          category: z.string().default("general").describe("Free-form category tag for filtering, e.g. `architecture`, `infra`, `process`. Default `general`."),
          importance: z.number().min(0).max(1).default(0.7).describe("Importance score in [0, 1] for `store`. Default 0.7 — decisions outrank typical facts (0.5)."),
          supersedes_id: z.string().default("").describe("For `store`: optional id of an earlier decision this one replaces. The older decision is marked superseded."),
          query: z.string().default("").describe("Search text (required for `action=\"search\"`). Tokenised by FTS5."),
          limit: z.number().int().min(1).max(50).default(10).describe("Maximum rows to return for `list`/`search` (1-50). Default 10."),
        },
        annotations: {
          title: "Architectural decision log",
          readOnlyHint: false,
          destructiveHint: false,
          idempotentHint: false,
          openWorldHint: false,
        },
        outputSchema: {
          message: z.string().describe("For `store`: `Decision stored with ID: <id>`. For `list`/`search`: markdown bullet list of decisions or `No decisions found.` Returns `Invalid action: ...` when `action` is unsupported."),
        },
      },
      async (params) => textResult(await handleDecisionsLog(decRepo, params))
    );
  • DecisionsRepo class providing store(), list(), and search() methods that the handler delegates to for database operations.
    export class DecisionsRepo {
      constructor(private db: Database.Database) {}
    
      private getOrCreateProject(rootPath: string): string {
        const row = this.db.prepare("SELECT id FROM projects WHERE root_path = ?").get(rootPath) as any;
        if (row) return row.id;
        const id = randomUUID();
        this.db.prepare("INSERT INTO projects (id, name, root_path) VALUES (?, ?, ?)").run(id, rootPath.split("/").pop() ?? rootPath, rootPath);
        return id;
      }
    
      store(projectPath: string, title: string, body: string, category = "general", importance = 0.7, supersedesId?: string): string {
        const projectId = this.getOrCreateProject(projectPath);
        const id = randomUUID();
        const now = nowIso();
        if (supersedesId) {
          this.db.prepare("UPDATE decisions SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL").run(now, supersedesId);
        }
        // Issue #12: scrub secrets from title and body at write time.
        const cleanTitle = scrubSecrets(title);
        const cleanBody = scrubSecrets(body);
        if (cleanTitle !== title) {
          logger.warn(`Secret pattern detected and scrubbed in decision title (id=${id})`);
        }
        if (hasPrivate(title)) {
          logger.warn(`Warning: <private> tags detected in decision title — tags do not redact in titles. Move sensitive content to body. (id=${id})`);
        }
        // Issue #4: set has_private flag on store.
        const hasPrivateFlag = hasPrivate(cleanBody) ? 1 : 0;
        this.db.prepare(`
          INSERT INTO decisions (id, project_id, title, body, category, importance_score, supersedes_id, has_private, created_at)
          VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        `).run(id, projectId, cleanTitle, cleanBody, category, importance, supersedesId ?? null, hasPrivateFlag, now);
        return id;
      }
    
      list(projectPath: string, limit = 10): any[] {
        const projectId = this.getOrCreateProject(projectPath);
        return this.db.prepare(`
          SELECT * FROM decisions WHERE project_id = ? AND deleted_at IS NULL
          ORDER BY importance_score DESC, created_at DESC LIMIT ?
        `).all(projectId, limit) as any[];
      }
    
      search(query: string, projectPath: string, limit = 10): any[] {
        const projectId = this.getOrCreateProject(projectPath);
        const ftsQuery = buildFtsQuery(query);
        if (!ftsQuery) return [];
        return this.db.prepare(`
          SELECT d.*, rank FROM decisions_fts fts
          JOIN decisions d ON d.rowid = fts.rowid
          WHERE decisions_fts MATCH ? AND d.project_id = ? AND d.deleted_at IS NULL
          ORDER BY rank LIMIT ?
        `).all(ftsQuery, projectId, limit) as any[];
      }
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations are present and the description adds significant behavioral context beyond them: it explains that store writes a row, list and search are read-only, decisions outrank memories and survive pruning longer. There is no contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a clear header and bullet-pointed actions, but it is slightly verbose. However, every sentence adds value, and it is front-loaded with the core concept.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the multi-action complexity and 9 parameters, the description covers all necessary aspects: action selection, parameter requirements per action, and behavioral notes. An output schema exists, so return values do not need to be described.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the description adds extra meaning with examples (e.g., 'Use Postgres over MySQL'), clarification of parameter scoping (e.g., 'required for store', 'ignored otherwise'), and default values with rationale (importance default 0.7 since decisions outrank typical facts).

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool is a 'Multi-action ADR log' and lists three specific actions (store, list, search) with brief explanations. It distinguishes itself from siblings by noting that decisions outrank free-form memories and pointing to pitfalls_log for recurring problems.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly guides when to use each action (e.g., 'record a new decision' for store, 'list recent decisions' for list) and when not to use ('Use pitfalls_log for recurring problems'). Also provides context on parameter requirements per action.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lfrmonteiro99/memento-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server