Skip to main content
Glama
TylerIlunga

Procore MCP Server

Execute Any Procore API Call

procore_api_call

Execute any Procore REST API call. Automatically handles OAuth, substitutes path placeholders, encodes nested query brackets, and returns parsed JSON with pagination and rate-limit metadata.

Instructions

Execute any Procore REST API call. This is the workhorse — first use the discover/search tools to find the right endpoint, then call it here. Handles OAuth automatically (uses the saved tokens), substitutes path placeholders, encodes nested query brackets (__ becomes [/]), and returns the parsed JSON response with pagination + rate-limit metadata.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
methodYesHTTP method
pathYesAPI path with placeholders, e.g. /rest/v1.0/projects/{project_id}/rfis
path_paramsNoSubstitutions for path placeholders, e.g. { project_id: '12345' }
query_paramsNoQuery parameters. Use double underscores for nested brackets: filters__status becomes filters[status]
bodyNoJSON request body for POST/PUT/PATCH calls
company_idNoOverride the default Procore-Company-Id header
pageNoPage number for paginated endpoints
per_pageNoItems per page (max 100)

Implementation Reference

  • Registration of the 'procore_api_call' tool on the MCP server, defining its input schema (method, path, path_params, query_params, body, company_id, page, per_page) via Zod validation.
    // 4. API Call (the core tool)
    server.registerTool(
      "procore_api_call",
      {
        title: "Execute Any Procore API Call",
        description:
          "Execute any Procore REST API call. This is the workhorse — first use the " +
          "discover/search tools to find the right endpoint, then call it here. " +
          "Handles OAuth automatically (uses the saved tokens), substitutes path " +
          "placeholders, encodes nested query brackets (`__` becomes `[`/`]`), and " +
          "returns the parsed JSON response with pagination + rate-limit metadata.",
        inputSchema: {
          method: z
            .enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
            .describe("HTTP method"),
          path: z
            .string()
            .describe(
              "API path with placeholders, e.g. /rest/v1.0/projects/{project_id}/rfis"
            ),
          path_params: z
            .record(z.string())
            .optional()
            .describe(
              "Substitutions for path placeholders, e.g. { project_id: '12345' }"
            ),
          query_params: z
            .record(z.union([z.string(), z.number(), z.boolean()]))
            .optional()
            .describe(
              "Query parameters. Use double underscores for nested brackets: filters__status becomes filters[status]"
            ),
          body: z
            .record(z.unknown())
            .optional()
            .describe("JSON request body for POST/PUT/PATCH calls"),
          company_id: z
            .number()
            .optional()
            .describe("Override the default Procore-Company-Id header"),
          page: z.number().optional().describe("Page number for paginated endpoints"),
          per_page: z
            .number()
            .optional()
            .describe("Items per page (max 100)"),
        },
        annotations: {
          title: "Procore API Call",
          readOnlyHint: false,
          destructiveHint: false,
          idempotentHint: false,
          openWorldHint: true,
        },
      },
      async (args) => {
        const text = await handleApiCall(args);
        return { content: [{ type: "text" as const, text }] };
      }
    );
  • Handler function that receives the tool arguments, delegates to the procoreApiCall function, formats the response (status, data, pagination, rate-limit warnings), and returns a string.
    export async function handleApiCall(args: {
      method: string;
      path: string;
      path_params?: Record<string, string>;
      query_params?: Record<string, string | number | boolean>;
      body?: Record<string, unknown>;
      company_id?: number;
      page?: number;
      per_page?: number;
    }): Promise<string> {
      const options: ApiCallOptions = {
        method: args.method,
        path: args.path,
        pathParams: args.path_params,
        queryParams: args.query_params,
        body: args.body,
        companyId: args.company_id,
        page: args.page,
        perPage: args.per_page,
      };
    
      try {
        const response = await procoreApiCall(options);
    
        const parts: string[] = [];
    
        // Status
        if (response.status >= 200 && response.status < 300) {
          parts.push(`Status: ${response.status} OK`);
        } else {
          parts.push(`Status: ${response.status}`);
        }
    
        // Data
        const dataStr = JSON.stringify(response.data, null, 2);
        if (dataStr.length > 50000) {
          parts.push(
            `\nResponse (truncated to 50KB):\n${dataStr.slice(0, 50000)}\n... (truncated)`
          );
        } else {
          parts.push(`\nResponse:\n${dataStr}`);
        }
    
        // Pagination
        if (response.pagination) {
          const p = response.pagination;
          parts.push(`\nPagination: page ${p.current_page}, ${p.per_page} per page`);
          if (p.total !== undefined) {
            parts.push(`Total items: ${p.total}`);
          }
          if (p.has_next) {
            parts.push(
              `Has more pages — call again with page=${p.current_page + 1}`
            );
          } else {
            parts.push("No more pages.");
          }
        }
    
        // Rate limit warning
        if (response.rate_limit && response.rate_limit.remaining < 20) {
          parts.push(
            `\n⚠ Rate limit: ${response.rate_limit.remaining}/${response.rate_limit.limit} requests remaining`
          );
        }
    
        return parts.join("\n");
      } catch (err) {
        const error = err as Error;
        return `Error: ${error.message}\n\nSuggestion: Check the endpoint path and parameters. Use procore_get_endpoint_details to verify the correct format.`;
      }
    }
  • Core API client function 'procoreApiCall' — handles path substitution, query string building, OAuth token management, rate-limit awareness, retry logic (429, 401 with token refresh, 5xx), response parsing, and pagination extraction.
    export async function procoreApiCall(
      options: ApiCallOptions
    ): Promise<ProcoreApiResponse> {
      const { method, pathParams, queryParams, body, page, perPage } = options;
      let { path, companyId } = options;
    
      // Substitute path parameters
      path = substitutePath(path, pathParams);
    
      // Build full URL
      const qs = buildQueryString(queryParams, page, perPage);
      const url = `${getApiBaseUrl()}${path}${qs}`;
    
      // Get auth token
      const token = await getValidAccessToken();
    
      // Build headers
      const headers: Record<string, string> = {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      };
    
      const effectiveCompanyId = companyId ?? getDefaultCompanyId();
      if (effectiveCompanyId) {
        headers["Procore-Company-Id"] = String(effectiveCompanyId);
      }
    
      // Wait if rate limited
      await waitForRateLimit();
    
      let lastError: Error | null = null;
      for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
        if (attempt > 0) {
          const delay = RETRY_BASE_MS * Math.pow(2, attempt - 1);
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
    
        try {
          const res = await fetch(url, {
            method: method.toUpperCase(),
            headers,
            body:
              body && ["POST", "PUT", "PATCH"].includes(method.toUpperCase())
                ? JSON.stringify(body)
                : undefined,
          });
    
          // Update rate limit state
          const rlRemaining = res.headers.get("X-Rate-Limit-Remaining");
          const rlLimit = res.headers.get("X-Rate-Limit-Limit");
          const rlReset = res.headers.get("X-Rate-Limit-Reset");
          if (rlRemaining !== null) {
            rateLimitState = {
              remaining: parseInt(rlRemaining, 10),
              limit: rlLimit ? parseInt(rlLimit, 10) : rateLimitState.limit,
              resetAt: rlReset ? parseInt(rlReset, 10) * 1000 : rateLimitState.resetAt,
            };
          }
    
          // Handle 429 rate limit
          if (res.status === 429) {
            const retryAfter = res.headers.get("Retry-After");
            const waitSec = retryAfter ? parseInt(retryAfter, 10) : 10;
            rateLimitState.remaining = 0;
            rateLimitState.resetAt = Date.now() + waitSec * 1000;
            if (attempt < MAX_RETRIES) continue;
          }
    
          // Handle 401 — try token refresh once
          if (res.status === 401 && attempt === 0) {
            const { refreshAccessToken } = await import("../auth/oauth.js");
            try {
              await refreshAccessToken();
              const newToken = await getValidAccessToken();
              headers.Authorization = `Bearer ${newToken}`;
              continue;
            } catch {
              // Refresh failed, return the 401
            }
          }
    
          // Handle 5xx with retry
          if (res.status >= 500 && attempt < MAX_RETRIES) {
            continue;
          }
    
          // Parse response
          const contentType = res.headers.get("content-type") || "";
          let data: unknown;
          if (contentType.includes("application/json")) {
            data = await res.json();
          } else {
            data = await res.text();
          }
    
          // Parse pagination from Link header
          const linkHeader = res.headers.get("Link");
          const totalHeader = res.headers.get("Total");
          const perPageHeader = res.headers.get("Per-Page");
          const links = parseLinkHeader(linkHeader);
    
          const pagination =
            linkHeader || totalHeader
              ? {
                  current_page: page || 1,
                  per_page: perPageHeader
                    ? parseInt(perPageHeader, 10)
                    : perPage || 20,
                  has_next: !!links.next,
                  total: totalHeader ? parseInt(totalHeader, 10) : undefined,
                }
              : undefined;
    
          const result: ProcoreApiResponse = {
            status: res.status,
            data,
            pagination,
            rate_limit: {
              remaining: rateLimitState.remaining,
              limit: rateLimitState.limit,
              reset_at: rateLimitState.resetAt,
            },
          };
    
          return result;
        } catch (err) {
          lastError = err as Error;
          if (attempt < MAX_RETRIES) continue;
        }
      }
    
      throw lastError || new Error("API call failed after retries");
    }
  • TypeScript interface 'ApiCallOptions' defining the shape of options passed to procoreApiCall: method, path, pathParams, queryParams, body, companyId, page, perPage.
    export interface ApiCallOptions {
      method: string;
      path: string;
      pathParams?: Record<string, string>;
      queryParams?: Record<string, string | number | boolean>;
      body?: Record<string, unknown>;
      companyId?: number;
      page?: number;
      perPage?: number;
    }
Behavior5/5

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

Annotations only provide readOnlyHint=false, destructiveHint=false, idempotentHint=false, openWorldHint=true. The description adds substantial behavioral context: automatic OAuth handling, path placeholder substitution, nested query bracket encoding (`__` to `[`/`]`), and return of parsed JSON with pagination and rate-limit metadata. This fully covers the tool's behavior beyond annotations.

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 four sentences, each adding value. The first sentence states purpose, second guides workflow, third details technical behaviors, fourth describes output format. No fluff or repetition.

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

Completeness4/5

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

Given no output schema, the description adequately describes the return value (parsed JSON with pagination and rate-limit metadata). However, it does not mention error handling or authentication failure scenarios. The tool is a generic catch-all, so slight incompleteness is acceptable. A 4 is appropriate.

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

Parameters3/5

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

Schema coverage is 100%, so the baseline is 3. The description adds context for how parameters work together (e.g., path_params, query_params with double underscores, pagination fields), but does not significantly expand on the schema descriptions themselves. It is adequate but not exceptional.

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 'Execute any Procore REST API call' and positions it as the workhorse tool. It distinguishes from sibling-specific tools by advising to use discover/search tools first and then call this endpoint. The verb 'execute' plus 'Procore REST API call' is specific and unique.

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

Usage Guidelines4/5

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

The description provides clear context: 'first use the discover/search tools to find the right endpoint, then call it here.' It explains when to use this tool (after discovery) and hints at alternatives (specific tools). However, it does not explicitly state when NOT to use (e.g., when a specific endpoint tool exists), so it loses a point.

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/TylerIlunga/procore-mcp-server'

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