diff --git a/manifest.json b/manifest.json index b024ae6..b606139 100644 --- a/manifest.json +++ b/manifest.json @@ -105,6 +105,34 @@ { "name": "aps_issues_docs", "description": "ACC Issues API quick-reference: project ID format, statuses, workflows, filters, 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/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)); diff --git a/src/aps-dm-helpers.ts b/src/aps-dm-helpers.ts index dcf9e03..f625704 100644 --- a/src/aps-dm-helpers.ts +++ b/src/aps-dm-helpers.ts @@ -488,6 +488,365 @@ 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 ──────────────────────────────── + +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."; + 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; +} + +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/aps-submittals-helpers.ts b/src/aps-submittals-helpers.ts new file mode 100644 index 0000000..9bacc2f --- /dev/null +++ b/src/aps-submittals-helpers.ts @@ -0,0 +1,359 @@ +/** + * APS ACC Submittals helpers: response summarisers, path builders, + * parameter validation, and quick‑reference docs. + */ + +// ── 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 ──────────────────────────────── + +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."; + 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; +} + +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 +`; diff --git a/src/index.ts b/src/index.ts index 56bb506..dfb2f5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,15 @@ * aps_issues_get_comments – list comments on an issue * aps_issues_create_comment – add a comment * aps_issues_docs – Issues API quick‑reference + * + * 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"; @@ -68,6 +77,17 @@ import { validateIssuesPath, ISSUES_DOCS, } from "./aps-issues-helpers.js"; +import { + summarizeSubmittalItems, + summarizeSubmittalPackages, + summarizeSubmittalSpecs, + summarizeSubmittalAttachments, + submittalPath, + validateSubmittalProjectId, + validateSubmittalItemId, + validateSubmittalPath, + SUBMITTALS_DOCS, +} from "./aps-submittals-helpers.js"; // ── Environment ────────────────────────────────────────────────── @@ -778,6 +798,205 @@ const TOOLS = [ "Call this before your first Issues interaction.", inputSchema: { type: "object" as const, properties: {} }, }, + + // ═══════════════════════════════════════════════════════════════ + // ── ACC Submittals tools ─────────────────────────────────────── + // ═══════════════════════════════════════════════════════════════ + + // 19 ── 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"], + }, + }, + + // 20 ── 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"], + }, + }, + + // 21 ── 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"], + }, + }, + + // 22 ── 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"], + }, + }, + + // 23 ── 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"], + }, + }, + + // 24 ── 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"], + }, + }, + + // 25 ── 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 ──────────────────────────────────────────────── @@ -1235,6 +1454,135 @@ async function handleTool( return ok(ISSUES_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 rawItemId = args.item_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + 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, { + 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 rawItemId = args.item_id as string; + const e1 = validateSubmittalProjectId(projectId); + if (e1) return fail(e1); + 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}/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}`); }