Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added core
Binary file not shown.
269 changes: 269 additions & 0 deletions packages/mcp-server/src/internal/constraint-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import { describe, it, expect } from "vitest";
import { z } from "zod";
import {
getConstraintKeysToFilter,
getConstraintParametersToInject,
} from "./constraint-helpers";

/**
* Test suite for constraint helper functions.
*
* These tests verify the logic for filtering schemas and injecting parameters
* when handling MCP constraints with parameter aliases (projectSlug → projectSlugOrId).
*/

describe("Constraint Helpers", () => {
// Mock tool schemas for testing
const schemaWithProjectSlugOrId = {
organizationSlug: z.string(),
projectSlugOrId: z.string().optional(),
query: z.string().optional(),
};

const schemaWithProjectSlug = {
organizationSlug: z.string(),
projectSlug: z.string().optional(),
query: z.string().optional(),
};

describe("getConstraintKeysToFilter", () => {
it("filters direct constraint matches", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: null,
regionUrl: null,
};

const keys = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlug,
);

expect(keys).toEqual(["organizationSlug"]);
});

it("applies projectSlug → projectSlugOrId alias when projectSlug is constrained", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "my-project",
regionUrl: null,
};

const keys = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlugOrId,
);

// Should filter both organizationSlug (direct match) and projectSlugOrId (alias)
expect(keys).toEqual(["organizationSlug", "projectSlugOrId"]);
});

it("does NOT apply alias when projectSlugOrId is explicitly constrained", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "project-a",
projectSlugOrId: "project-b", // Explicit constraint takes precedence
regionUrl: null,
};

const keys = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlugOrId,
);

// Should filter organizationSlug and projectSlugOrId (explicit), but NOT the alias
expect(keys).toEqual(["organizationSlug", "projectSlugOrId"]);
});

it("handles null/falsy constraint values", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: null, // Falsy - should not trigger alias
regionUrl: null,
};

const keys = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlugOrId,
);

expect(keys).toEqual(["organizationSlug"]);
});

it("handles empty string as falsy", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "", // Empty string is falsy
regionUrl: null,
};

const keys = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlugOrId,
);

// Empty string is falsy, so no alias should be applied
expect(keys).toEqual(["organizationSlug"]);
});

it("only filters parameters that exist in the tool schema", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "my-project",
regionUrl: "https://us.sentry.io",
};

const schemaWithoutRegion = {
organizationSlug: z.string(),
query: z.string(),
};

const keys = getConstraintKeysToFilter(constraints, schemaWithoutRegion);

// regionUrl not in schema, so it shouldn't be filtered
expect(keys).toEqual(["organizationSlug"]);
});
});

describe("getConstraintParametersToInject", () => {
it("injects direct constraint matches", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: null,
regionUrl: null,
};

const params = getConstraintParametersToInject(
constraints,
schemaWithProjectSlug,
);

expect(params).toEqual({
organizationSlug: "my-org",
});
});

it("injects projectSlug as projectSlugOrId when alias applies", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "my-project",
regionUrl: null,
};

const params = getConstraintParametersToInject(
constraints,
schemaWithProjectSlugOrId,
);

expect(params).toEqual({
organizationSlug: "my-org",
projectSlugOrId: "my-project", // Injected via alias
});
});

it("respects explicit projectSlugOrId constraint over alias", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "project-a",
projectSlugOrId: "project-b", // Explicit constraint
regionUrl: null,
};

const params = getConstraintParametersToInject(
constraints,
schemaWithProjectSlugOrId,
);

expect(params).toEqual({
organizationSlug: "my-org",
projectSlugOrId: "project-b", // Explicit wins, not alias
});
});

it("handles null/falsy constraint values", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: null,
regionUrl: null,
};

const params = getConstraintParametersToInject(
constraints,
schemaWithProjectSlugOrId,
);

expect(params).toEqual({
organizationSlug: "my-org",
});
});

it("only injects parameters that exist in the tool schema", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "my-project",
regionUrl: "https://us.sentry.io",
};

const schemaWithoutRegion = {
organizationSlug: z.string(),
query: z.string(),
};

const params = getConstraintParametersToInject(
constraints,
schemaWithoutRegion,
);

// regionUrl not in schema, so it shouldn't be injected
expect(params).toEqual({
organizationSlug: "my-org",
});
});
});

describe("Consistency between filtering and injection", () => {
it("ensures filtered keys match injected keys", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "my-project",
regionUrl: null,
};

const keysToFilter = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlugOrId,
);
const paramsToInject = getConstraintParametersToInject(
constraints,
schemaWithProjectSlugOrId,
);

// Every key that's filtered should have a corresponding injected parameter
const injectedKeys = Object.keys(paramsToInject);
expect(keysToFilter.sort()).toEqual(injectedKeys.sort());
});

it("handles explicit constraint precedence consistently", () => {
const constraints = {
organizationSlug: "my-org",
projectSlug: "project-a",
projectSlugOrId: "project-b",
regionUrl: null,
};

const keysToFilter = getConstraintKeysToFilter(
constraints,
schemaWithProjectSlugOrId,
);
const paramsToInject = getConstraintParametersToInject(
constraints,
schemaWithProjectSlugOrId,
);

// Both should handle the explicit constraint the same way
expect(keysToFilter).toContain("projectSlugOrId");
expect(paramsToInject.projectSlugOrId).toBe("project-b");
expect(paramsToInject.projectSlug).toBeUndefined();
});
});
});
107 changes: 107 additions & 0 deletions packages/mcp-server/src/internal/constraint-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Constraint application helpers for MCP server configuration.
*
* These functions handle the logic for filtering tool schemas and injecting
* constraint parameters, including support for parameter aliases (e.g., projectSlug → projectSlugOrId).
*/
import type { Constraints } from "../types";
import type { z } from "zod";

/**
* Determines which tool parameter keys should be filtered out of the schema
* because they will be injected from constraints.
*
* Handles parameter aliases: when a projectSlug constraint exists and the tool
* has a projectSlugOrId parameter, the alias will be applied UNLESS projectSlugOrId
* is explicitly constrained with a truthy value.
*
* @param constraints - The active constraints (org, project, region)
* @param toolInputSchema - The tool's input schema definition
* @returns Array of parameter keys that should be filtered from the schema
*
* @example
* ```typescript
* const constraints = { projectSlug: "my-project", organizationSlug: "my-org" };
* const schema = { organizationSlug: z.string(), projectSlugOrId: z.string() };
* const keys = getConstraintKeysToFilter(constraints, schema);
* // Returns: ["organizationSlug", "projectSlugOrId"]
* // projectSlugOrId is included because projectSlug constraint will map to it
* ```
*/
export function getConstraintKeysToFilter(
constraints: Constraints & Record<string, string | null | undefined>,
toolInputSchema: Record<string, z.ZodType>,
): string[] {
return Object.entries(constraints).flatMap(([key, value]) => {
if (!value) return [];

const keys: string[] = [];

// If this constraint key exists in the schema, include it
if (key in toolInputSchema) {
keys.push(key);
}

// Special handling: projectSlug constraint can also apply to projectSlugOrId parameter
// Only add the alias to filter if projectSlugOrId isn't being explicitly constrained
if (
key === "projectSlug" &&
"projectSlugOrId" in toolInputSchema &&
!("projectSlugOrId" in constraints && constraints.projectSlugOrId)
) {
keys.push("projectSlugOrId");
}

return keys;
});
}

/**
* Builds the constraint parameters that should be injected into tool calls.
*
* Handles parameter aliases: when a projectSlug constraint exists and the tool
* has a projectSlugOrId parameter, the constraint value will be injected as
* projectSlugOrId UNLESS projectSlugOrId is explicitly constrained with a truthy value.
*
* @param constraints - The active constraints (org, project, region)
* @param toolInputSchema - The tool's input schema definition
* @returns Object mapping parameter names to constraint values
*
* @example
* ```typescript
* const constraints = { projectSlug: "my-project", organizationSlug: "my-org" };
* const schema = { organizationSlug: z.string(), projectSlugOrId: z.string() };
* const params = getConstraintParametersToInject(constraints, schema);
* // Returns: { organizationSlug: "my-org", projectSlugOrId: "my-project" }
* // projectSlug constraint is injected as projectSlugOrId parameter
* ```
*/
export function getConstraintParametersToInject(
constraints: Constraints & Record<string, string | null | undefined>,
toolInputSchema: Record<string, z.ZodType>,
): Record<string, string> {
return Object.fromEntries(
Object.entries(constraints).flatMap(([key, value]) => {
if (!value) return [];

const entries: [string, string][] = [];

// If this constraint key exists in the schema, add it
if (key in toolInputSchema) {
entries.push([key, value]);
}

// Special handling: projectSlug constraint can also apply to projectSlugOrId parameter
// Only apply alias if the target parameter isn't already being constrained with a truthy value
if (
key === "projectSlug" &&
"projectSlugOrId" in toolInputSchema &&
!("projectSlugOrId" in constraints && constraints.projectSlugOrId)
) {
entries.push(["projectSlugOrId", value]);
}

return entries;
}),
);
}
Loading