Skip to main content
Glama

list_projects

Retrieve and display Basecamp projects with details like status, purpose, and dock tools. Supports pagination and JSON output for project management.

Instructions

Returns id, name, status, timestamps, purpose, clients_enabled, bookmarked, and enabled dock tools. Supports pagination and JSON output.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
limitNo
pageNo
include_archivedNo
include_dockNo
dock_detailNo
formatNo

Implementation Reference

  • The async handler function for the 'list_projects' tool. It fetches projects from the Basecamp API using bcRequestWithHeaders, filters by archived status and limit, processes dock tools, and returns either a formatted table or JSON output with pagination info.
      async (args) => {
        const {
          limit,
          page,
          include_archived,
          include_dock,
          dock_detail,
          format,
        } = args;
        const wantDock = include_dock ?? true;
        const fmt = format ?? "table";
    
        const { data, headers } = await bcRequestWithHeaders<any[]>(
          "GET",
          "/projects.json",
          undefined,
          page ? { page } : undefined
        );
    
        let projects = Array.isArray(data) ? data : [];
        if (!include_archived)
          projects = projects.filter((p) => p.status === "active");
        if (limit) projects = projects.slice(0, limit);
    
        const rows = projects.map((p) => {
          const item: any = {
            id: p.id,
            name: p.name,
            status: p.status,
            created_at: p.created_at,
            updated_at: p.updated_at,
            description: p.description ?? "",
            purpose: p.purpose ?? "",
            clients_enabled: Boolean(p.clients_enabled),
            bookmarked: Boolean(p.bookmarked),
          };
          if (wantDock && Array.isArray(p.dock)) {
            item.dock_tools = p.dock
              .filter((d: any) => d?.enabled)
              .map((d: any) => d.name);
            if (dock_detail) item.dock = p.dock; // raw dock objects (includes todoset URL)
          }
          return item;
        });
    
        const nextPage = parseNextPage(headers.get("Link"));
    
        if (fmt === "json") {
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    page: page ?? 1,
                    nextPage,
                    count: rows.length,
                    projects: rows,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        }
    
        const trunc = (s: string, n: number) =>
          s && s.length > n ? s.slice(0, n - 1) + "…" : s || "";
        const header = [
          "ID".padEnd(12),
          "STATUS".padEnd(9),
          "CLIENTS".padEnd(8),
          "BOOKMK".padEnd(6),
          "CREATED".padEnd(10),
          "UPDATED".padEnd(10),
          "NAME".padEnd(28),
          "PURPOSE".padEnd(8),
          wantDock ? "DOCK" : "",
        ]
          .filter(Boolean)
          .join("  ");
    
        const lines = [
          `Page: ${page ?? 1}${nextPage ? `  |  Next page: ${nextPage}` : ""}`,
          "",
          header,
          "-".repeat(120),
          ...rows.map((r: any) =>
            [
              String(r.id).padEnd(12),
              String(r.status ?? "").padEnd(9),
              String(r.clients_enabled).padEnd(8),
              String(r.bookmarked).padEnd(6),
              (r.created_at ?? "").slice(0, 10).padEnd(10),
              (r.updated_at ?? "").slice(0, 10).padEnd(10),
              trunc(r.name ?? "", 28).padEnd(28),
              trunc(r.purpose ?? "", 8).padEnd(8),
              wantDock ? trunc((r.dock_tools ?? []).join(","), 28) : "",
            ]
              .filter(Boolean)
              .join("  ")
          ),
        ].join("\n");
    
        return { content: [{ type: "text", text: lines }] };
      }
    );
  • Zod input schema defining optional parameters for listing projects: limit, page, include_archived, include_dock, dock_detail, and format.
    inputSchema: {
      limit: z.number().int().positive().max(200).optional(),
      page: z.number().int().positive().optional(),
      include_archived: z.boolean().optional(),
      include_dock: z.boolean().optional(),
      dock_detail: z.boolean().optional(), // NEW: include raw dock
      format: z.enum(["table", "json"]).optional(),
    },
  • Registration of the 'list_projects' tool on the MCP server, including title, description, input schema, and handler function.
    server.registerTool(
      "list_projects",
      {
        title: "List Basecamp projects (rich)",
        description:
          "Returns id, name, status, timestamps, purpose, clients_enabled, bookmarked, and enabled dock tools. Supports pagination and JSON output.",
        inputSchema: {
          limit: z.number().int().positive().max(200).optional(),
          page: z.number().int().positive().optional(),
          include_archived: z.boolean().optional(),
          include_dock: z.boolean().optional(),
          dock_detail: z.boolean().optional(), // NEW: include raw dock
          format: z.enum(["table", "json"]).optional(),
        },
      },
      async (args) => {
        const {
          limit,
          page,
          include_archived,
          include_dock,
          dock_detail,
          format,
        } = args;
        const wantDock = include_dock ?? true;
        const fmt = format ?? "table";
    
        const { data, headers } = await bcRequestWithHeaders<any[]>(
          "GET",
          "/projects.json",
          undefined,
          page ? { page } : undefined
        );
    
        let projects = Array.isArray(data) ? data : [];
        if (!include_archived)
          projects = projects.filter((p) => p.status === "active");
        if (limit) projects = projects.slice(0, limit);
    
        const rows = projects.map((p) => {
          const item: any = {
            id: p.id,
            name: p.name,
            status: p.status,
            created_at: p.created_at,
            updated_at: p.updated_at,
            description: p.description ?? "",
            purpose: p.purpose ?? "",
            clients_enabled: Boolean(p.clients_enabled),
            bookmarked: Boolean(p.bookmarked),
          };
          if (wantDock && Array.isArray(p.dock)) {
            item.dock_tools = p.dock
              .filter((d: any) => d?.enabled)
              .map((d: any) => d.name);
            if (dock_detail) item.dock = p.dock; // raw dock objects (includes todoset URL)
          }
          return item;
        });
    
        const nextPage = parseNextPage(headers.get("Link"));
    
        if (fmt === "json") {
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  {
                    page: page ?? 1,
                    nextPage,
                    count: rows.length,
                    projects: rows,
                  },
                  null,
                  2
                ),
              },
            ],
          };
        }
    
        const trunc = (s: string, n: number) =>
          s && s.length > n ? s.slice(0, n - 1) + "…" : s || "";
        const header = [
          "ID".padEnd(12),
          "STATUS".padEnd(9),
          "CLIENTS".padEnd(8),
          "BOOKMK".padEnd(6),
          "CREATED".padEnd(10),
          "UPDATED".padEnd(10),
          "NAME".padEnd(28),
          "PURPOSE".padEnd(8),
          wantDock ? "DOCK" : "",
        ]
          .filter(Boolean)
          .join("  ");
    
        const lines = [
          `Page: ${page ?? 1}${nextPage ? `  |  Next page: ${nextPage}` : ""}`,
          "",
          header,
          "-".repeat(120),
          ...rows.map((r: any) =>
            [
              String(r.id).padEnd(12),
              String(r.status ?? "").padEnd(9),
              String(r.clients_enabled).padEnd(8),
              String(r.bookmarked).padEnd(6),
              (r.created_at ?? "").slice(0, 10).padEnd(10),
              (r.updated_at ?? "").slice(0, 10).padEnd(10),
              trunc(r.name ?? "", 28).padEnd(28),
              trunc(r.purpose ?? "", 8).padEnd(8),
              wantDock ? trunc((r.dock_tools ?? []).join(","), 28) : "",
            ]
              .filter(Boolean)
              .join("  ")
          ),
        ].join("\n");
    
        return { content: [{ type: "text", text: lines }] };
      }
    );

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/craigashields/basecamp-mcp'

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