Execute Any Procore API Call
procore_api_callExecute 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
| Name | Required | Description | Default |
|---|---|---|---|
| method | Yes | HTTP method | |
| path | Yes | API path with placeholders, e.g. /rest/v1.0/projects/{project_id}/rfis | |
| path_params | No | Substitutions for path placeholders, e.g. { project_id: '12345' } | |
| query_params | No | Query parameters. Use double underscores for nested brackets: filters__status becomes filters[status] | |
| body | No | JSON request body for POST/PUT/PATCH calls | |
| company_id | No | Override the default Procore-Company-Id header | |
| page | No | Page number for paginated endpoints | |
| per_page | No | Items per page (max 100) |
Implementation Reference
- src/tools/registry.ts:108-166 (registration)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 }] }; } ); - src/tools/handlers/api-call.ts:4-75 (handler)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.`; } } - src/api/client.ts:77-209 (helper)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"); } - src/api/types.ts:23-32 (schema)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; }