From 515c7f923a423a3bc9de36ec0b4f6ce4b531e28e Mon Sep 17 00:00:00 2001 From: Pablo Derendinger <98769613+pderendinger-everse@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:18:58 -0300 Subject: [PATCH 1/4] Adds ACC Submittals API tools Adds a set of tools for interacting with the ACC Submittals API, including raw request, item listing, package listing, specs listing, and attachment retrieval. Provides helper functions for constructing API paths, summarizing responses, and validating input. Also includes quick-reference documentation for the Submittals API. --- manifest.json | 28 ++++ src/aps-helpers.ts | 339 +++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 347 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 713 insertions(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index a7d2718..760c9fe 100644 --- a/manifest.json +++ b/manifest.json @@ -60,6 +60,34 @@ { "name": "aps_docs", "description": "APS Data Management quick-reference: ID formats, workflows, API paths, error troubleshooting." + }, + { + "name": "aps_submittals_request", + "description": "Raw ACC Submittals API call. Full JSON response. Power-user tool for any submittals endpoint." + }, + { + "name": "aps_list_submittal_items", + "description": "List submittal items — compact summary with title, number, status, priority, revision. Supports filtering." + }, + { + "name": "aps_get_submittal_item", + "description": "Get full details for a single submittal item by ID." + }, + { + "name": "aps_list_submittal_packages", + "description": "List submittal packages — compact summary with title, identifier, spec section." + }, + { + "name": "aps_list_submittal_specs", + "description": "List spec sections for submittals — identifier (e.g. 033100), title, dates." + }, + { + "name": "aps_get_submittal_item_attachments", + "description": "Get attachments for a submittal item — file names, URNs, revision numbers." + }, + { + "name": "aps_submittals_docs", + "description": "ACC Submittals API quick-reference: endpoints, statuses, custom numbering, workflow, key concepts." } ], "keywords": [ diff --git a/src/aps-helpers.ts b/src/aps-helpers.ts index dcf9e03..25db51f 100644 --- a/src/aps-helpers.ts +++ b/src/aps-helpers.ts @@ -488,6 +488,345 @@ export function validateItemId(id: string): string | null { return null; } +// ══════════════════════════════════════════════════════════════════ +// ── ACC Submittals helpers ──────────────────────────────────────── +// ══════════════════════════════════════════════════════════════════ + +// ── Submittal types ────────────────────────────────────────────── + +export interface SubmittalItemSummary { + id: string; + title: string; + number?: string; + spec_identifier?: string; + subsection?: string; + type?: string; + status?: string; + priority?: string; + revision?: number; + description?: string; + manager?: string; + subcontractor?: string; + due_date?: string; + required_on_job_date?: string; + response?: string; + response_comment?: string; + created_at?: string; + updated_at?: string; + package_title?: string; + package_id?: string; +} + +export interface SubmittalPackageSummary { + id: string; + title: string; + identifier?: number; + spec_identifier?: string; + description?: string; + created_at?: string; + updated_at?: string; +} + +export interface SubmittalSpecSummary { + id: string; + identifier: string; + title: string; + created_at?: string; + updated_at?: string; +} + +export interface SubmittalAttachmentSummary { + id: string; + name: string; + urn?: string; + upload_urn?: string; + revision?: number; + category?: string; + created_at?: string; + created_by?: string; +} + +// ── ACC project‑ID helper ──────────────────────────────────────── + +/** + * Convert a project ID from DM format ('b.uuid') to ACC format ('uuid'). + * If the ID already lacks the 'b.' prefix, it is returned as‑is. + */ +export function toAccProjectId(projectId: string): string { + return projectId.replace(/^b\./, ""); +} + +// ── Submittal base path builder ────────────────────────────────── + +const SUBMITTALS_BASE = "construction/submittals/v2"; + +/** Build the Submittals API path for a given project. */ +export function submittalPath(projectId: string, subPath: string): string { + const pid = toAccProjectId(projectId); + const sub = subPath.replace(/^\//, ""); + return `${SUBMITTALS_BASE}/projects/${pid}/${sub}`; +} + +// ── Submittal response summarisers ─────────────────────────────── + +/** + * Summarise the paginated response from GET /items. + * ACC Submittals API returns `{ pagination, results }` with camelCase fields. + */ +export function summarizeSubmittalItems(raw: unknown): { + pagination: { total: number; limit: number; offset: number }; + items: SubmittalItemSummary[]; +} { + const r = raw as Record | undefined; + const pagination = r?.pagination as Record | undefined; + const results = Array.isArray(r?.results) + ? (r!.results as Record[]) + : []; + + const items: SubmittalItemSummary[] = results.map((item) => ({ + id: item.id as string, + title: (item.title as string) ?? "(untitled)", + number: + (item.customIdentifierHumanReadable as string) ?? + (item.customIdentifier as string) ?? + undefined, + spec_identifier: (item.specIdentifier as string) ?? undefined, + subsection: (item.subsection as string) ?? undefined, + type: (item.typeValue as string) ?? (item.type as string) ?? undefined, + status: (item.statusValue as string) ?? (item.status as string) ?? undefined, + priority: (item.priorityValue as string) ?? (item.priority as string) ?? undefined, + revision: (item.revision as number) ?? undefined, + description: (item.description as string) ?? undefined, + manager: (item.manager as string) ?? undefined, + subcontractor: (item.subcontractor as string) ?? undefined, + due_date: (item.dueDate as string) ?? undefined, + required_on_job_date: (item.requiredOnJobDate as string) ?? undefined, + response: (item.responseValue as string) ?? undefined, + response_comment: (item.responseComment as string) ?? undefined, + created_at: (item.createdAt as string) ?? undefined, + updated_at: (item.updatedAt as string) ?? undefined, + package_title: (item.packageTitle as string) ?? undefined, + package_id: (item.package as string) ?? undefined, + })); + + return { + pagination: { + total: (pagination?.totalResults as number) ?? items.length, + limit: (pagination?.limit as number) ?? 0, + offset: (pagination?.offset as number) ?? 0, + }, + items, + }; +} + +/** Summarise the paginated response from GET /packages. */ +export function summarizeSubmittalPackages(raw: unknown): { + pagination: { total: number; limit: number; offset: number }; + packages: SubmittalPackageSummary[]; +} { + const r = raw as Record | undefined; + const pagination = r?.pagination as Record | undefined; + const results = Array.isArray(r?.results) + ? (r!.results as Record[]) + : []; + + const packages: SubmittalPackageSummary[] = results.map((pkg) => ({ + id: pkg.id as string, + title: (pkg.title as string) ?? "(untitled)", + identifier: (pkg.identifier as number) ?? undefined, + spec_identifier: (pkg.specIdentifier as string) ?? undefined, + description: (pkg.description as string) ?? undefined, + created_at: (pkg.createdAt as string) ?? undefined, + updated_at: (pkg.updatedAt as string) ?? undefined, + })); + + return { + pagination: { + total: (pagination?.totalResults as number) ?? packages.length, + limit: (pagination?.limit as number) ?? 0, + offset: (pagination?.offset as number) ?? 0, + }, + packages, + }; +} + +/** Summarise the paginated response from GET /specs. */ +export function summarizeSubmittalSpecs(raw: unknown): { + pagination: { total: number; limit: number; offset: number }; + specs: SubmittalSpecSummary[]; +} { + const r = raw as Record | undefined; + const pagination = r?.pagination as Record | undefined; + const results = Array.isArray(r?.results) + ? (r!.results as Record[]) + : []; + + const specs: SubmittalSpecSummary[] = results.map((spec) => ({ + id: spec.id as string, + identifier: (spec.identifier as string) ?? "", + title: (spec.title as string) ?? "(untitled)", + created_at: (spec.createdAt as string) ?? undefined, + updated_at: (spec.updatedAt as string) ?? undefined, + })); + + return { + pagination: { + total: (pagination?.totalResults as number) ?? specs.length, + limit: (pagination?.limit as number) ?? 0, + offset: (pagination?.offset as number) ?? 0, + }, + specs, + }; +} + +/** Summarise the response from GET /items/:itemId/attachments. */ +export function summarizeSubmittalAttachments(raw: unknown): { + attachments: SubmittalAttachmentSummary[]; +} { + // The response may be { results: [...] } or just an array + const r = raw as Record | undefined; + const results = Array.isArray(r?.results) + ? (r!.results as Record[]) + : Array.isArray(raw) + ? (raw as Record[]) + : []; + + const attachments: SubmittalAttachmentSummary[] = results.map((att) => ({ + id: att.id as string, + name: (att.name as string) ?? "(unknown)", + urn: (att.urn as string) ?? undefined, + upload_urn: (att.uploadUrn as string) ?? undefined, + revision: (att.revision as number) ?? undefined, + category: (att.categoryValue as string) ?? (att.category as string) ?? undefined, + created_at: (att.createdAt as string) ?? undefined, + created_by: (att.createdBy as string) ?? undefined, + })); + + return { attachments }; +} + +// ── Submittal‑specific validation ──────────────────────────────── + +export function validateSubmittalProjectId(id: string): string | null { + if (!id) return "project_id is required."; + // Accept both 'b.uuid' (DM format) and plain UUID (ACC format) + return null; +} + +export function validateSubmittalItemId(id: string): string | null { + if (!id) return "item_id is required."; + return null; +} + +export function validateSubmittalPath(path: string): string | null { + if (!path || typeof path !== "string") return "path is required and must be a non‑empty string."; + if (path.includes("..")) return "path must not contain '..'."; + return null; +} + +// ── Quick‑reference documentation ──────────────────────────────── + +export const SUBMITTALS_DOCS = `# ACC Submittals API – Quick Reference + +## Overview +The ACC Submittals API lets you read and create submittal items, packages, spec sections, +attachments, and responses for Autodesk Construction Cloud (ACC Build) projects. + +## Project ID Format +- The Submittals API uses **UUID** project IDs (e.g. \`abc12345-6789-…\`). +- If you have a Data Management project ID with \`b.\` prefix, the prefix is stripped automatically. +- Account ID is also a UUID (hub ID without \`b.\` prefix). + +## Authentication +- **2‑legged OAuth** with scopes: \`data:read\` (read), \`data:write\` (create/update). +- Uses the same token as the Data Management tools. + +## API Base Path +\`https://developer.api.autodesk.com/construction/submittals/v2/projects/{projectId}/…\` + +## Available Endpoints + +### Read Endpoints +| Action | Method | Path | +|--------|--------|------| +| List submittal items | GET | items | +| Get submittal item | GET | items/{itemId} | +| List packages | GET | packages | +| Get package | GET | packages/{packageId} | +| List spec sections | GET | specs | +| Get item type | GET | item-types/{id} | +| List item types | GET | item-types | +| List responses | GET | responses | +| Get response | GET | responses/{id} | +| Item attachments | GET | items/{itemId}/attachments | +| Project metadata | GET | metadata | +| Manager settings | GET | settings/mappings | +| Current user perms | GET | users/me | +| Next custom number | GET | items:next-custom-identifier | + +### Write Endpoints +| Action | Method | Path | +|--------|--------|------| +| Create submittal item | POST | items | +| Create spec section | POST | specs | +| Validate custom number | POST | items:validate-custom-identifier | + +## Common Query Parameters (GET items) +- \`limit\` – items per page (default 20, max 200) +- \`offset\` – pagination offset +- \`filter[statusId]\` – filter by status: 1=Required, 2=Open, 3=Closed, 4=Void, 5=Empty, 6=Draft +- \`filter[packageId]\` – filter by package ID +- \`filter[reviewResponseId]\` – filter by review response ID +- \`filter[specId]\` – filter by spec section ID +- \`sort\` – sort by field (e.g. \`title\`, \`createdAt\`) + +## Submittal Item Statuses +| ID | Status | +|----|--------| +| 1 | Required | +| 2 | Open | +| 3 | Closed | +| 4 | Void | +| 5 | Empty | +| 6 | Draft | + +## Submittal Item Priorities +| ID | Priority | +|----|----------| +| 1 | Low | +| 2 | Normal | +| 3 | High | + +## Custom Numbering +- **Global format**: items get a global sequential number. +- **Spec section format**: items numbered as \`-\` (e.g. \`033100-01\`). +- \`customIdentifier\` – the sequential number portion. +- \`customIdentifierHumanReadable\` – the full display number. + +## Typical Workflow +\`\`\` +1. aps_list_hubs / aps_list_projects → get project ID +2. aps_list_submittal_specs project_id → see spec sections +3. aps_list_submittal_packages project_id → see packages +4. aps_list_submittal_items project_id → browse items (with filters) +5. aps_get_submittal_item project_id + id → item details +6. aps_get_submittal_item_attachments → view attachments +\`\`\` + +## Key Concepts +- **Submittal Item**: A document (shop drawing, product data, sample, etc.) that requires review. +- **Package**: Groups related submittal items for batch submission. +- **Spec Section**: A specification division (e.g. "033100 – Structural Concrete"). +- **Response**: The review outcome (e.g. Approved, Revise and Resubmit). +- **Review Step / Task**: Multi‑step review workflow with assigned reviewers. + +## Full Documentation +- Field Guide: https://aps.autodesk.com/en/docs/acc/v1/overview/field-guide/submittals/ +- API Reference: https://aps.autodesk.com/en/docs/acc/v1/reference/http/submittals-items-GET/ +- Create Item Tutorial: https://aps.autodesk.com/en/docs/acc/v1/tutorials/submittals/create-submittal-item/ +- Data Schema: https://developer.api.autodesk.com/data-connector/v1/doc/schema?name=submittalsacc&format=html +`; + // ── Quick‑reference documentation ──────────────────────────────── export const APS_DOCS = `# APS Data Management – Quick Reference diff --git a/src/index.ts b/src/index.ts index 990f2cb..9a40277 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ /** * MCP server for Autodesk Platform Services (APS). * - * Tools: + * Data Management Tools: * aps_get_token – verify credentials / obtain 2‑legged token * aps_dm_request – raw Data Management API (power‑user) * aps_list_hubs – simplified hub listing @@ -11,6 +11,15 @@ * aps_get_item_details – single file / item metadata * aps_get_folder_tree – recursive folder tree * aps_docs – APS quick‑reference documentation + * + * Submittals Tools: + * aps_submittals_request – raw Submittals API (power‑user) + * aps_list_submittal_items – list submittal items + * aps_get_submittal_item – single submittal item details + * aps_list_submittal_packages – list submittal packages + * aps_list_submittal_specs – list spec sections + * aps_get_submittal_item_attachments – attachments for a submittal item + * aps_submittals_docs – Submittals quick‑reference documentation */ import { Server } from "@modelcontextprotocol/sdk/server"; @@ -34,6 +43,16 @@ import { validateFolderId, validateItemId, APS_DOCS, + // ── Submittals ── + summarizeSubmittalItems, + summarizeSubmittalPackages, + summarizeSubmittalSpecs, + summarizeSubmittalAttachments, + submittalPath, + validateSubmittalProjectId, + validateSubmittalItemId, + validateSubmittalPath, + SUBMITTALS_DOCS, } from "./aps-helpers.js"; // ── Environment ────────────────────────────────────────────────── @@ -284,6 +303,205 @@ const TOOLS = [ "Call this before your first APS interaction or when unsure about ID formats or API paths.", inputSchema: { type: "object" as const, properties: {} }, }, + + // ═══════════════════════════════════════════════════════════════ + // ── ACC Submittals tools ─────────────────────────────────────── + // ═══════════════════════════════════════════════════════════════ + + // 10 ── aps_submittals_request (raw / power‑user) + { + name: "aps_submittals_request", + description: + "Call any ACC Submittals API endpoint. " + + "This is the raw / power‑user tool – it returns the full JSON response. " + + "Prefer the simplified tools (aps_list_submittal_items, aps_list_submittal_packages, etc.) for everyday use. " + + "Use this tool when you need full control: pagination, POST/PATCH, or endpoints not covered by simplified tools " + + "(e.g. metadata, settings/mappings, users/me, item-types, responses).\n\n" + + "The base path is: construction/submittals/v2/projects/{projectId}/\n" + + "You only need to provide the sub‑path after 'projects/{projectId}/' (e.g. 'items', 'packages', 'specs').", + inputSchema: { + type: "object" as const, + properties: { + project_id: { + type: "string", + description: + "Project ID – UUID format (e.g. 'abc12345-6789-…'). " + + "If you have a DM project ID with 'b.' prefix, it will be stripped automatically.", + }, + method: { + type: "string", + enum: ["GET", "POST"], + description: "HTTP method. Default: GET.", + }, + path: { + type: "string", + description: + "Sub‑path relative to 'projects/{projectId}/' " + + "(e.g. 'items', 'packages', 'specs', 'items/{itemId}', 'metadata', 'responses', 'item-types').", + }, + query: { + type: "object", + description: + "Optional query parameters as key/value pairs (e.g. { \"limit\": \"50\", \"offset\": \"0\", \"filter[statusId]\": \"2\" }).", + additionalProperties: { type: "string" }, + }, + body: { + type: "object", + description: "Optional JSON body for POST requests.", + }, + }, + required: ["project_id", "path"], + }, + }, + + // 11 ── aps_list_submittal_items + { + name: "aps_list_submittal_items", + description: + "List submittal items in an ACC project. " + + "Returns a compact summary: title, number, spec section, type, status, priority, revision, dates. " + + "Supports filtering by status, package, spec section, and review response.", + inputSchema: { + type: "object" as const, + properties: { + project_id: { + type: "string", + description: "Project ID (UUID or 'b.' prefixed – auto‑converted).", + }, + filter_status: { + type: "string", + description: + "Filter by status ID: 1=Required, 2=Open, 3=Closed, 4=Void, 5=Empty, 6=Draft. " + + "Omit to return all statuses.", + }, + filter_package_id: { + type: "string", + description: "Filter by package UUID. Omit to return items from all packages.", + }, + filter_spec_id: { + type: "string", + description: "Filter by spec section UUID. Omit to return all spec sections.", + }, + limit: { + type: "number", + description: "Max items per page (1–200). Default 20.", + }, + offset: { + type: "number", + description: "Pagination offset. Default 0.", + }, + }, + required: ["project_id"], + }, + }, + + // 12 ── aps_get_submittal_item + { + name: "aps_get_submittal_item", + description: + "Get full details for a single submittal item by ID. " + + "Returns the complete item object from the API.", + inputSchema: { + type: "object" as const, + properties: { + project_id: { + type: "string", + description: "Project ID (UUID or 'b.' prefixed – auto‑converted).", + }, + item_id: { + type: "string", + description: "Submittal item UUID.", + }, + }, + required: ["project_id", "item_id"], + }, + }, + + // 13 ── aps_list_submittal_packages + { + name: "aps_list_submittal_packages", + description: + "List submittal packages in an ACC project. " + + "Returns a compact summary: title, identifier, spec section, description, dates.", + inputSchema: { + type: "object" as const, + properties: { + project_id: { + type: "string", + description: "Project ID (UUID or 'b.' prefixed – auto‑converted).", + }, + limit: { + type: "number", + description: "Max items per page (1–200). Default 20.", + }, + offset: { + type: "number", + description: "Pagination offset. Default 0.", + }, + }, + required: ["project_id"], + }, + }, + + // 14 ── aps_list_submittal_specs + { + name: "aps_list_submittal_specs", + description: + "List spec sections for submittals in an ACC project. " + + "Returns a compact summary: identifier (e.g. '033100'), title, dates. " + + "Spec sections are the specification divisions that submittal items are organised under.", + inputSchema: { + type: "object" as const, + properties: { + project_id: { + type: "string", + description: "Project ID (UUID or 'b.' prefixed – auto‑converted).", + }, + limit: { + type: "number", + description: "Max items per page (1–200). Default 20.", + }, + offset: { + type: "number", + description: "Pagination offset. Default 0.", + }, + }, + required: ["project_id"], + }, + }, + + // 15 ── aps_get_submittal_item_attachments + { + name: "aps_get_submittal_item_attachments", + description: + "Get attachments for a specific submittal item. " + + "Returns file names, URNs, revision numbers, and categories. " + + "Use the URN to download the attachment via the Data Management API.", + inputSchema: { + type: "object" as const, + properties: { + project_id: { + type: "string", + description: "Project ID (UUID or 'b.' prefixed – auto‑converted).", + }, + item_id: { + type: "string", + description: "Submittal item UUID.", + }, + }, + required: ["project_id", "item_id"], + }, + }, + + // 16 ── aps_submittals_docs + { + name: "aps_submittals_docs", + description: + "Return ACC Submittals API quick‑reference documentation: " + + "endpoints, query parameters, statuses, custom numbering, typical workflow, and key concepts. " + + "Call this before your first Submittals interaction or when unsure about Submittals API usage.", + inputSchema: { type: "object" as const, properties: {} }, + }, ]; // ── Tool handlers ──────────────────────────────────────────────── @@ -442,6 +660,133 @@ async function handleTool( return ok(APS_DOCS); } + // ═══════════════════════════════════════════════════════════════ + // ── ACC Submittals handlers ──────────────────────────────────── + // ═══════════════════════════════════════════════════════════════ + + // ── aps_submittals_request ────────────────────────────────── + if (name === "aps_submittals_request") { + const projectId = args.project_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + const subPath = args.path as string; + const pathErr = validateSubmittalPath(subPath); + if (pathErr) return fail(pathErr); + + const method = (args.method as string) ?? "GET"; + const query = args.query as Record | undefined; + const body = args.body as Record | undefined; + const t = await token(); + const fullPath = submittalPath(projectId, subPath); + const data = await apsDmRequest( + method as "GET" | "POST" | "PATCH" | "DELETE", + fullPath, + t, + { query, body, headers: { "Content-Type": "application/json" } }, + ); + return json(data); + } + + // ── aps_list_submittal_items ──────────────────────────────── + if (name === "aps_list_submittal_items") { + const projectId = args.project_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + + const query: Record = {}; + const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 200); + query.limit = String(limit); + if (args.offset != null) query.offset = String(args.offset); + if (args.filter_status) query["filter[statusId]"] = args.filter_status as string; + if (args.filter_package_id) query["filter[packageId]"] = args.filter_package_id as string; + if (args.filter_spec_id) query["filter[specId]"] = args.filter_spec_id as string; + + const t = await token(); + const raw = await apsDmRequest("GET", submittalPath(projectId, "items"), t, { + query, + headers: { "Content-Type": "application/json" }, + }); + return json(summarizeSubmittalItems(raw)); + } + + // ── aps_get_submittal_item ────────────────────────────────── + if (name === "aps_get_submittal_item") { + const projectId = args.project_id as string; + const itemId = args.item_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + const e2 = validateSubmittalItemId(itemId); + if (e2) return fail(e2); + + const t = await token(); + const raw = await apsDmRequest("GET", submittalPath(projectId, `items/${itemId}`), t, { + headers: { "Content-Type": "application/json" }, + }); + return json(raw); + } + + // ── aps_list_submittal_packages ───────────────────────────── + if (name === "aps_list_submittal_packages") { + const projectId = args.project_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + + const query: Record = {}; + const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 200); + query.limit = String(limit); + if (args.offset != null) query.offset = String(args.offset); + + const t = await token(); + const raw = await apsDmRequest("GET", submittalPath(projectId, "packages"), t, { + query, + headers: { "Content-Type": "application/json" }, + }); + return json(summarizeSubmittalPackages(raw)); + } + + // ── aps_list_submittal_specs ──────────────────────────────── + if (name === "aps_list_submittal_specs") { + const projectId = args.project_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + + const query: Record = {}; + const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 200); + query.limit = String(limit); + if (args.offset != null) query.offset = String(args.offset); + + const t = await token(); + const raw = await apsDmRequest("GET", submittalPath(projectId, "specs"), t, { + query, + headers: { "Content-Type": "application/json" }, + }); + return json(summarizeSubmittalSpecs(raw)); + } + + // ── aps_get_submittal_item_attachments ────────────────────── + if (name === "aps_get_submittal_item_attachments") { + const projectId = args.project_id as string; + const itemId = args.item_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + const e2 = validateSubmittalItemId(itemId); + if (e2) return fail(e2); + + const t = await token(); + const raw = await apsDmRequest( + "GET", + submittalPath(projectId, `items/${itemId}/attachments`), + t, + { headers: { "Content-Type": "application/json" } }, + ); + return json(summarizeSubmittalAttachments(raw)); + } + + // ── aps_submittals_docs ───────────────────────────────────── + if (name === "aps_submittals_docs") { + return ok(SUBMITTALS_DOCS); + } + return fail(`Unknown tool: ${name}`); } From e5d2ffaaaeb3197d71ee7165efb4ef6603d798ab Mon Sep 17 00:00:00 2001 From: Pablo Derendinger <98769613+pderendinger-everse@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:06:24 -0300 Subject: [PATCH 2/4] Adds submittals helpers to server build Includes the submittals helper functions in the server build process. This ensures that the server component has access to necessary utilities for handling submittals logic. --- scripts/pack-mcpb.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pack-mcpb.mjs b/scripts/pack-mcpb.mjs index 6d7647c..adf9b6f 100644 --- a/scripts/pack-mcpb.mjs +++ b/scripts/pack-mcpb.mjs @@ -24,7 +24,7 @@ fs.copyFileSync(path.join(root, "manifest.json"), path.join(buildDir, "manifest. // Copy server entry (dist -> server/) const distDir = path.join(root, "dist"); -for (const name of ["index.js", "aps-auth.js", "aps-issues-helpers.js", "aps-dm-helpers.js"]) { +for (const name of ["index.js", "aps-auth.js", "aps-issues-helpers.js", "aps-dm-helpers.js", "aps-submittals-helpers.js"]) { const src = path.join(distDir, name); if (!fs.existsSync(src)) throw new Error(`Build first: missing ${src}`); fs.copyFileSync(src, path.join(buildDir, "server", name)); From 63f5612707108f9b55d27b717f18905a0d2b115a Mon Sep 17 00:00:00 2001 From: Pablo Derendinger <98769613+pderendinger-everse@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:20:48 -0300 Subject: [PATCH 3/4] Validates submittal IDs and encodes them. Adds validation to `project_id` and `item_id` to prevent directory traversal attacks and ensures that `item_id` is properly encoded for the API request. This change enhances security and ensures that the `item_id` is correctly formatted when making requests to the APS DM API. --- src/aps-dm-helpers.ts | 22 +++++++++++++++++++++- src/index.ts | 10 ++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/aps-dm-helpers.ts b/src/aps-dm-helpers.ts index 25db51f..f625704 100644 --- a/src/aps-dm-helpers.ts +++ b/src/aps-dm-helpers.ts @@ -707,14 +707,34 @@ export function summarizeSubmittalAttachments(raw: unknown): { // ── Submittal‑specific validation ──────────────────────────────── +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +function containsTraversalTokens(value: string): boolean { + return ( + value.includes("/") || + value.includes("\\") || + value.toLowerCase().includes("%2f") || + value.includes("..") + ); +} + export function validateSubmittalProjectId(id: string): string | null { if (!id) return "project_id is required."; - // Accept both 'b.uuid' (DM format) and plain UUID (ACC format) + if (containsTraversalTokens(id)) + return "project_id contains disallowed characters ('/', '\\', '%2F', or '..')."; + // Accept 'b.' (DM format) or plain UUID (ACC format) + const bare = id.startsWith("b.") ? id.slice(2) : id; + if (!UUID_RE.test(bare)) + return "project_id must be a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) optionally prefixed with 'b.'."; return null; } export function validateSubmittalItemId(id: string): string | null { if (!id) return "item_id is required."; + if (containsTraversalTokens(id)) + return "item_id contains disallowed characters ('/', '\\', '%2F', or '..')."; + if (!UUID_RE.test(id)) + return "item_id must be a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)."; return null; } diff --git a/src/index.ts b/src/index.ts index 6a85b3d..dfb2f5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1506,11 +1506,12 @@ async function handleTool( // ── aps_get_submittal_item ────────────────────────────────── if (name === "aps_get_submittal_item") { const projectId = args.project_id as string; - const itemId = args.item_id as string; + const rawItemId = args.item_id as string; const e1 = validateSubmittalProjectId(projectId); if (e1) return fail(e1); - const e2 = validateSubmittalItemId(itemId); + const e2 = validateSubmittalItemId(rawItemId); if (e2) return fail(e2); + const itemId = encodeURIComponent(rawItemId); const t = await token(); const raw = await apsDmRequest("GET", submittalPath(projectId, `items/${itemId}`), t, { @@ -1560,11 +1561,12 @@ async function handleTool( // ── aps_get_submittal_item_attachments ────────────────────── if (name === "aps_get_submittal_item_attachments") { const projectId = args.project_id as string; - const itemId = args.item_id as string; + const rawItemId = args.item_id as string; const e1 = validateSubmittalProjectId(projectId); if (e1) return fail(e1); - const e2 = validateSubmittalItemId(itemId); + const e2 = validateSubmittalItemId(rawItemId); if (e2) return fail(e2); + const itemId = encodeURIComponent(rawItemId); const t = await token(); const raw = await apsDmRequest( From c2288aeccd2044b0e9f1d78d7470ff0f343a932b Mon Sep 17 00:00:00 2001 From: Pablo Derendinger <98769613+pderendinger-everse@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:36:52 -0300 Subject: [PATCH 4/4] Validates submittal project and item IDs Adds validation to ensure project and item IDs are properly formatted UUIDs and do not contain path traversal tokens. This prevents potential security vulnerabilities and ensures data integrity. --- src/aps-submittals-helpers.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/aps-submittals-helpers.ts b/src/aps-submittals-helpers.ts index fde4715..9bacc2f 100644 --- a/src/aps-submittals-helpers.ts +++ b/src/aps-submittals-helpers.ts @@ -218,14 +218,34 @@ export function summarizeSubmittalAttachments(raw: unknown): { // ── Submittal‑specific validation ──────────────────────────────── +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +function containsTraversalTokens(value: string): boolean { + return ( + value.includes("/") || + value.includes("\\") || + value.toLowerCase().includes("%2f") || + value.includes("..") + ); +} + export function validateSubmittalProjectId(id: string): string | null { if (!id) return "project_id is required."; - // Accept both 'b.uuid' (DM format) and plain UUID (ACC format) + if (containsTraversalTokens(id)) + return "project_id contains disallowed characters ('/', '\\', '%2F', or '..')."; + // Accept 'b.' (DM format) or plain UUID (ACC format) + const bare = id.startsWith("b.") ? id.slice(2) : id; + if (!UUID_RE.test(bare)) + return "project_id must be a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) optionally prefixed with 'b.'."; return null; } export function validateSubmittalItemId(id: string): string | null { if (!id) return "item_id is required."; + if (containsTraversalTokens(id)) + return "item_id contains disallowed characters ('/', '\\', '%2F', or '..')."; + if (!UUID_RE.test(id)) + return "item_id must be a UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)."; return null; }