Skip to main content
Glama

project_map

Generate a compact indented outline of folders, file titles, tags, and IDs to quickly orient yourself in a vault or project.

Instructions

Return a compact indented outline of folders, file titles, tags, and IDs in a single dense block — substantially fewer tokens than the equivalent list_files JSON for the same scope. Read-only; no side effects, auth, or rate limits. Capped at max_lines (default 5000); the response reports est_tokens and emits a warning field if it exceeds CTXNEST_PROJECT_TOKEN_WARN. project_id: null = KB only; omit = everything. Defaults: include_tags=true, show_titles=true. Use to orient yourself in an unfamiliar vault or project; for keyword lookup use search.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idNoRestrict to a single project. Pass null for knowledge-base-only files. Omit for everything.
include_tagsNoAppend #tags inline. Default true. Set false to shrink the outline.
show_titlesNoShow file titles instead of filenames. Default true.
max_linesNoHard cap on output lines (each line ≈ one folder or file). Default 5000.

Implementation Reference

  • The core handler function `projectMap()` that builds the folder/file outline from the SQLite database. It queries files (optionally scoped by project_id), locates each file under its root (knowledge or project), builds an in-memory tree, and renders a compact indented outline with file IDs, titles, tags, and folder hierarchy.
    export function projectMap(opts: ProjectMapOptions): ProjectMapResult {
      const includeTags = opts.include_tags !== false;
      const showTitles = opts.show_titles !== false;
      const maxLines = opts.max_lines ?? 5000;
    
      const db = getDatabase();
      let sql = "SELECT * FROM files WHERE 1=1";
      const params: unknown[] = [];
      if (opts.project_id !== undefined) {
        if (opts.project_id === null) {
          sql += " AND project_id IS NULL";
        } else {
          sql += " AND project_id = ?";
          params.push(opts.project_id);
        }
      }
      sql += " ORDER BY path ASC";
      const files = db.prepare(sql).all(...params) as FileRecord[];
    
      const projects = db.prepare("SELECT * FROM projects").all() as ProjectRecord[];
      const projectsById = new Map(projects.map((p) => [p.id, p]));
    
      const tagsByFileId = includeTags ? getTagsForFiles(files.map((f) => f.id)) : new Map();
    
      const tree: TreeNode = emptyNode();
      for (const file of files) {
        const location = locateFile(file, projectsById, opts.dataDir);
        const leaf: FileLeaf = {
          id: file.id,
          title: file.title,
          filename: basename(file.path),
          tags: tagsByFileId.get(file.id) ?? [],
        };
        insert(tree, location, leaf);
      }
    
      const out: string[] = [];
      const budget = { maxLines };
      let totalFolders = 0;
      let totalFiles = 0;
      let roots = 0;
    
      // Render each root as a top-level group.
      const rootNames = [...tree.dirs.keys()].sort();
      for (const rootName of rootNames) {
        if (out.length >= budget.maxLines) break;
        out.push(`${rootName}/`);
        roots++;
        totalFolders++;
        const sub = renderNode(tree.dirs.get(rootName)!, "  ", showTitles, out, budget);
        totalFolders += sub.folders;
        totalFiles += sub.files;
      }
    
      const truncated = out.length >= maxLines && (totalFiles < files.length);
      if (truncated) {
        out.push(`... (truncated at ${maxLines} lines; use list_files for full enumeration)`);
      }
    
      const outline = out.join("\n");
      return {
        outline,
        stats: {
          files: totalFiles,
          folders: Math.max(0, totalFolders - roots), // don't count the root labels themselves
          roots,
          truncated,
        },
        est_tokens: Math.ceil(outline.length / 4),
      };
    }
  • Type definitions: `ProjectMapOptions` (input params including project_id filter, include_tags, show_titles, max_lines), `ProjectMapStats` (counts of files/folders/roots and truncation flag), and `ProjectMapResult` (outline string, stats, estimated token count).
    export interface ProjectMapOptions {
      dataDir: string;
      /**
       * Filter scope:
       *   - undefined / "all": every file in every root
       *   - "knowledge": only files with project_id IS NULL
       *   - { project_id: N }: only that project (use null for knowledge-only)
       */
      project_id?: number | null;
      /** Attach `tags` to each file leaf. Default true. */
      include_tags?: boolean;
      /** Render the title before the filename. Default true. */
      show_titles?: boolean;
      /** Cap output size. Default 5000 lines (one line per file/folder). */
      max_lines?: number;
    }
    
    export interface ProjectMapStats {
      files: number;
      folders: number;
      roots: number;
      truncated: boolean;
    }
    
    export interface ProjectMapResult {
      /** Indented text outline — the primary payload for agent consumption. */
      outline: string;
      stats: ProjectMapStats;
      /**
       * Token estimate of `outline` (chars/4 heuristic). Lets the caller decide
       * whether the map fits its context budget before passing it along.
       */
      est_tokens: number;
  • MCP tool registration for 'project_map' using server.tool(). Defines the Zod schema for parameters (project_id, include_tags, show_titles, max_lines) and the async handler that calls the core `projectMap()` function, wrapping the result with token warnings.
    server.tool(
      "project_map",
      "Return a compact indented outline of folders, file titles, tags, and IDs in a single dense block — substantially fewer tokens than the equivalent `list_files` JSON for the same scope. Read-only; no side effects, auth, or rate limits. Capped at `max_lines` (default 5000); the response reports `est_tokens` and emits a `warning` field if it exceeds `CTXNEST_PROJECT_TOKEN_WARN`. `project_id: null` = KB only; omit = everything. Defaults: include_tags=true, show_titles=true. Use to orient yourself in an unfamiliar vault or project; for keyword lookup use `search`.",
      {
        project_id: z.number().nullable().optional().describe("Restrict to a single project. Pass null for knowledge-base-only files. Omit for everything."),
        include_tags: z.boolean().optional().describe("Append #tags inline. Default true. Set false to shrink the outline."),
        show_titles: z.boolean().optional().describe("Show file titles instead of filenames. Default true."),
        max_lines: z.number().optional().describe("Hard cap on output lines (each line ≈ one folder or file). Default 5000."),
      },
      async ({ project_id, include_tags, show_titles, max_lines }) => {
        const opts: any = { dataDir };
        if (project_id !== undefined) opts.project_id = project_id;
        if (include_tags !== undefined) opts.include_tags = include_tags;
        if (show_titles !== undefined) opts.show_titles = show_titles;
        if (max_lines !== undefined) opts.max_lines = max_lines;
        const result = projectMap(opts);
        const warning = tokenWarning(result.est_tokens);
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(
                {
                  stats: result.stats,
                  est_tokens: result.est_tokens,
                  outline: result.outline,
                  ...(warning ? { warning } : {}),
                },
                null,
                2
              ),
            },
          ],
        };
      }
    );
  • Helper functions: `emptyNode()` creates a tree node, `stripRoot()` strips a known root prefix from a file path returning relative segments, `locateFile()` resolves a file's location under knowledge or project roots, `insert()` places a file leaf into the tree, and `renderNode()` recursively renders the tree as indented text lines.
    function emptyNode(): TreeNode {
      return { dirs: new Map(), files: [] };
    }
    
    /**
     * Strip a known root prefix from an absolute file path. Returns the segments
     * relative to that root, or null if the path doesn't sit under the root.
     */
    function stripRoot(filePath: string, root: string): string[] | null {
      const rootWithSep = root.endsWith(sep) ? root : root + sep;
      if (filePath === root) return [];
      if (!filePath.startsWith(rootWithSep)) return null;
      return filePath.slice(rootWithSep.length).split(sep).filter(Boolean);
  • The `getTagsForFiles()` helper is used by projectMap to bulk-fetch tags for all files in a single query, avoiding N+1 round-trips.
    export function getTagsForFiles(fileIds: number[]): Map<number, string[]> {
      const out = new Map<number, string[]>();
      if (fileIds.length === 0) return out;
      const db = getDatabase();
      const placeholders = fileIds.map(() => "?").join(",");
      const rows = db
        .prepare(
          `SELECT ft.file_id AS file_id, t.name AS name
           FROM file_tags ft
           JOIN tags t ON t.id = ft.tag_id
           WHERE ft.file_id IN (${placeholders})
           ORDER BY t.name ASC`
        )
        .all(...fileIds) as { file_id: number; name: string }[];
      for (const r of rows) {
        const arr = out.get(r.file_id);
        if (arr) arr.push(r.name);
        else out.set(r.file_id, [r.name]);
      }
      return out;
    }
Behavior5/5

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

No annotations provided, but description fully covers behavior: read-only, no side effects/auth/rate limits, capped at max_lines (default 5000), reports est_tokens, emits warning if exceeded, and explains project_id semantics (null vs omit).

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

Conciseness5/5

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

The description is a single dense paragraph with no wasted words. It front-loads the primary benefit and purpose, and every sentence contributes essential information.

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?

Despite no output schema, the description conveys what the response includes (indented outline, est_tokens, warning field) and the use case. For a tool with 4 params, this is thorough.

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?

All 4 parameters have schema descriptions (100% coverage), and the description adds value by explaining defaults (include_tags=true, show_titles=true, max_lines=5000) and project_id nuances beyond the schema.

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 returns 'a compact indented outline of folders, file titles, tags, and IDs in a single dense block'. It distinguishes from sibling tools like list_files by noting it uses substantially fewer tokens.

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?

Explicit guidance: 'Use to orient yourself in an unfamiliar vault or project; for keyword lookup use `search`.' Also states it is read-only with no side effects, making usage conditions clear.

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/safiyu/ctxnest'

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