feat(mcp): add routing attribute MCP tools for organizations#102
feat(mcp): add routing attribute MCP tools for organizations#102joeauyeung wants to merge 2 commits into
Conversation
Add 15 MCP tools wrapping the Cal.com API v2 organization attributes endpoints: - get_org_attributes, get_org_attribute, create_org_attribute, update_org_attribute, delete_org_attribute - get_org_attribute_options, get_assigned_attribute_options, get_assigned_attribute_options_by_slug, create_org_attribute_option, update_org_attribute_option, delete_org_attribute_option - get_user_attribute_options, assign_attribute_to_user, update_user_attribute_assignment, remove_attribute_from_user All tools are registered with appropriate MCP annotations (READ_ONLY, CREATE, UPDATE, DESTRUCTIVE) and include unit tests. Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| export async function getOrgAttribute(params: { orgId: number; attributeId: string }) { | ||
| try { | ||
| const data = await calApi(`organizations/${params.orgId}/attributes/${params.attributeId}`); | ||
| return ok(data); | ||
| } catch (err) { | ||
| return handleError("get_org_attribute", err); | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 Missing path sanitization on string parameters interpolated into API URL paths
Every string parameter (attributeId, attributeSlug, optionId, attributeOptionId) is interpolated directly into URL path templates without calling sanitizePathSegment(). The rest of the codebase consistently sanitizes all user-supplied string path segments — bookings.ts sanitizes bookingUid (bookings.ts:86), routing-forms.ts sanitizes routingFormId (routing-forms.ts:90, routing-forms.ts:38) — to prevent path traversal attacks. A malicious attributeId like ../../admin would cause the API call to target an unintended endpoint. This affects 11 calApi() call sites across all functions that take string path parameters.
Affected call sites (11 instances)
- Line 42:
attributeIdingetOrgAttribute - Line 126:
attributeIdinupdateOrgAttribute - Line 150:
attributeIdindeleteOrgAttribute - Line 184:
attributeIdingetOrgAttributeOptions - Line 208:
attributeIdingetAssignedAttributeOptions - Line 234:
attributeSlugingetAssignedAttributeOptionsBySlug - Line 267:
attributeIdincreateOrgAttributeOption - Lines 305:
attributeId+optionIdinupdateOrgAttributeOption - Lines 339:
attributeId+optionIdindeleteOrgAttributeOption - Line 440:
attributeOptionIdinupdateUserAttributeAssignment - Line 477:
attributeOptionIdinremoveAttributeFromUser
Prompt for agents
The new attributes.ts file does not use sanitizePathSegment() for any of its string path parameters (attributeId, attributeSlug, optionId, attributeOptionId), unlike the rest of the codebase which consistently sanitizes all user-supplied strings before interpolating into URL paths (see bookings.ts and routing-forms.ts for the established pattern).
To fix:
1. Import sanitizePathSegment from ../../utils/path-sanitizer.js
2. In every function that interpolates a string parameter into a URL path, call sanitizePathSegment() on it before interpolation. For example, in getOrgAttribute: const attrId = sanitizePathSegment(params.attributeId); then use attrId in the template literal.
3. Apply this to all 11 calApi() call sites that use attributeId, attributeSlug, optionId, or attributeOptionId in URL paths.
Note: orgId and userId are numbers (typed as z.number().int()) so they don't need sanitization.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in cd0fee2. Added sanitizePathSegment to all 11 string path param interpolation sites (attributeId, attributeSlug, optionId, attributeOptionId), matching the established pattern in bookings.ts and routing-forms.ts.
…ute tools Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
There was a problem hiding this comment.
2 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mcp-server/src/tools/organizations/attributes.ts">
<violation number="1" location="apps/mcp-server/src/tools/organizations/attributes.ts:259">
P2: `create_org_attribute_option` allows missing `slug`, but the Cal.com endpoint requires it, causing avoidable API validation failures.</violation>
<violation number="2" location="apps/mcp-server/src/tools/organizations/attributes.ts:410">
P1: `assign_attribute_to_user` omits required `attributeId` in request payload, so assignments can fail against the API contract.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| }) { | ||
| try { | ||
| const body: Record<string, unknown> = {}; | ||
| body.attributeOptionId = params.attributeOptionId; |
There was a problem hiding this comment.
P1: assign_attribute_to_user omits required attributeId in request payload, so assignments can fail against the API contract.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mcp-server/src/tools/organizations/attributes.ts, line 410:
<comment>`assign_attribute_to_user` omits required `attributeId` in request payload, so assignments can fail against the API contract.</comment>
<file context>
@@ -0,0 +1,499 @@
+}) {
+ try {
+ const body: Record<string, unknown> = {};
+ body.attributeOptionId = params.attributeOptionId;
+ if (params.value !== undefined) body.value = params.value;
+ const data = await calApi(`organizations/${params.orgId}/attributes/options/${params.userId}`, {
</file context>
| .string() | ||
| .describe("Attribute ID. Use get_org_attributes to find this — never guess."), | ||
| value: z.string().describe("Value for the new option"), | ||
| slug: z.string().optional().describe("URL-friendly slug for the option"), |
There was a problem hiding this comment.
P2: create_org_attribute_option allows missing slug, but the Cal.com endpoint requires it, causing avoidable API validation failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mcp-server/src/tools/organizations/attributes.ts, line 259:
<comment>`create_org_attribute_option` allows missing `slug`, but the Cal.com endpoint requires it, causing avoidable API validation failures.</comment>
<file context>
@@ -0,0 +1,499 @@
+ .string()
+ .describe("Attribute ID. Use get_org_attributes to find this — never guess."),
+ value: z.string().describe("Value for the new option"),
+ slug: z.string().optional().describe("URL-friendly slug for the option"),
+};
+
</file context>
Summary
Adds 15 MCP tools wrapping the Cal.com API v2 organization attributes endpoints for reading and writing routing attributes per user.
New file
src/tools/organizations/attributes.tsfollowing existing patterns frommemberships.ts/routing-forms.ts:Attribute CRUD —
get_org_attributes,get_org_attribute,create_org_attribute,update_org_attribute,delete_org_attributeAttribute option management —
get_org_attribute_options,get_assigned_attribute_options,get_assigned_attribute_options_by_slug,create_org_attribute_option,update_org_attribute_option,delete_org_attribute_optionUser attribute assignments —
get_user_attribute_options,assign_attribute_to_user,update_user_attribute_assignment,remove_attribute_from_userAll tools registered in
register-tools.tswith appropriate MCP annotations (READ_ONLY,CREATE,UPDATE,DESTRUCTIVE). Descriptions follow existing style with hints like "Use get_me to obtain your organizationId — never guess." Unit tests cover success + error paths for every handler (60 tests).Link to Devin session: https://app.devin.ai/sessions/8b5a7ba58d3241f5a2e23cc52155d1eb
Requested by: @joeauyeung