Conversation
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.
WalkthroughThis PR introduces comprehensive ACC Submittals support to the APS MCP server, adding seven new tools with corresponding helper types, validators, summarizers, and documentation. All changes are additive with no removal of existing functionality. Changes
Sequence DiagramsequenceDiagram
participant Client as MCP Client
participant Handler as Tool Handler
participant Validator as Validator
participant APS as APS API
participant Summarizer as Response Summarizer
Client->>Handler: Call aps_list_submittal_items<br/>(projectId, filters, pagination)
Handler->>Validator: validateSubmittalProjectId(projectId)
Validator-->>Handler: Validation result
alt Validation passes
Handler->>APS: GET /projects/{projectId}/submittals/items<br/>(with filters & pagination)
APS-->>Handler: Raw JSON response
Handler->>Summarizer: summarizeSubmittalItems(raw)
Summarizer-->>Handler: Structured response<br/>(pagination + items)
Handler-->>Client: Summarized submittal items
else Validation fails
Handler-->>Client: Error message
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 712-783: The handlers aps_get_submittal_item and
aps_get_submittal_item_attachments currently interpolate raw args.item_id into
submittalPath, which can produce malformed paths for values with "/" or other
reserved chars; URL-encode the itemId (e.g., const itemId =
encodeURIComponent(args.item_id as string)) before calling submittalPath (and
keep existing validateSubmittalItemId checks), so both submittalPath(projectId,
`items/${itemId}`) and submittalPath(projectId, `items/${itemId}/attachments`)
use the encoded id.
🧹 Nitpick comments (1)
src/aps-helpers.ts (1)
551-568: Encode the project ID insubmittalPathfor safer URL construction.Even if project IDs are expected to be UUIDs, encoding/trim guards against stray whitespace or unexpected characters that would break the path.
🔧 Suggested change
export function submittalPath(projectId: string, subPath: string): string { - const pid = toAccProjectId(projectId); + const pid = encodeURIComponent(toAccProjectId(projectId).trim()); const sub = subPath.replace(/^\//, ""); return `${SUBMITTALS_BASE}/projects/${pid}/${sub}`; }
| // ── 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<string, string> = {}; | ||
| 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<string, string> = {}; | ||
| 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)); | ||
| } |
There was a problem hiding this comment.
Encode item_id before interpolating into submittals paths.
item_id is user input; if it contains / or other reserved characters, the path becomes malformed or targets the wrong endpoint. Encoding (or stricter validation) avoids this.
🔧 Suggested change
if (name === "aps_get_submittal_item") {
const projectId = args.project_id as string;
const itemId = args.item_id as string;
@@
- const raw = await apsDmRequest("GET", submittalPath(projectId, `items/${itemId}`), t, {
+ const encodedItemId = encodeURIComponent(itemId);
+ const raw = await apsDmRequest("GET", submittalPath(projectId, `items/${encodedItemId}`), t, {
headers: { "Content-Type": "application/json" },
});
return json(raw);
}
@@
if (name === "aps_get_submittal_item_attachments") {
const projectId = args.project_id as string;
const itemId = args.item_id as string;
@@
- const raw = await apsDmRequest(
- "GET",
- submittalPath(projectId, `items/${itemId}/attachments`),
+ const encodedItemId = encodeURIComponent(itemId);
+ const raw = await apsDmRequest(
+ "GET",
+ submittalPath(projectId, `items/${encodedItemId}/attachments`),
t,
{ headers: { "Content-Type": "application/json" } },
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ── 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<string, string> = {}; | |
| 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<string, string> = {}; | |
| 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_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 encodedItemId = encodeURIComponent(itemId); | |
| const raw = await apsDmRequest("GET", submittalPath(projectId, `items/${encodedItemId}`), 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<string, string> = {}; | |
| 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<string, string> = {}; | |
| 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 encodedItemId = encodeURIComponent(itemId); | |
| const raw = await apsDmRequest( | |
| "GET", | |
| submittalPath(projectId, `items/${encodedItemId}/attachments`), | |
| t, | |
| { headers: { "Content-Type": "application/json" } }, | |
| ); | |
| return json(summarizeSubmittalAttachments(raw)); | |
| } |
🤖 Prompt for AI Agents
In `@src/index.ts` around lines 712 - 783, The handlers aps_get_submittal_item and
aps_get_submittal_item_attachments currently interpolate raw args.item_id into
submittalPath, which can produce malformed paths for values with "/" or other
reserved chars; URL-encode the itemId (e.g., const itemId =
encodeURIComponent(args.item_id as string)) before calling submittalPath (and
keep existing validateSubmittalItemId checks), so both submittalPath(projectId,
`items/${itemId}`) and submittalPath(projectId, `items/${itemId}/attachments`)
use the encoded id.
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.
Summary by CodeRabbit