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
28 changes: 22 additions & 6 deletions mcp-worker/src/apiClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { UserProps, DevCycleJWTClaims } from './types'
import { IDevCycleApiClient } from '../../src/mcp/api/interface'
import { getErrorMessage, ensureError } from '../../src/mcp/utils/api'
import {
getErrorMessage,
ensureError,
projectKeyFromArgs,
MISSING_PROJECT_KEY_ERROR,
} from '../../src/mcp/utils/api'
import { setMCPHeaders, setMCPToolCommand } from '../../src/mcp/utils/headers'

/**
Expand Down Expand Up @@ -42,12 +47,10 @@ export class WorkerApiClient implements IDevCycleApiClient {
requiresProject: boolean = true,
): Promise<T> {
const authToken = this.getAuthToken()
const projectKey = await this.getProjectKey()
const projectKey = await this.resolveProjectKey(args)

if (requiresProject && !projectKey) {
throw new Error(
'No project key found, please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}

// Set MCP analytics headers for this specific tool operation
Expand Down Expand Up @@ -96,7 +99,7 @@ export class WorkerApiClient implements IDevCycleApiClient {
)

const orgId = this.getOrgId()
const projectKey = await this.getProjectKey()
const projectKey = await this.resolveProjectKey(args)
const link = dashboardLink(orgId, projectKey, result)

return {
Expand All @@ -115,6 +118,19 @@ export class WorkerApiClient implements IDevCycleApiClient {
return this.props.tokenSet.accessToken
}

/**
* Resolve the project for a call: the tool's own projectKey argument wins,
* otherwise fall back to the session's selected project / JWT claims.
*
* The argument takes priority because a host may start a new MCP session
* per request, in which case nothing survives from a prior select_project.
*/
private async resolveProjectKey(
args: unknown,
): Promise<string | undefined> {
return projectKeyFromArgs(args) ?? (await this.getProjectKey())
}

/**
* Get the project key from McpAgent state first, then fall back to JWT claims
*/
Expand Down
1 change: 1 addition & 0 deletions mcp-worker/src/projectSelectionTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export function registerProjectSelectionTools(
description: [
'Select a project to use for subsequent MCP operations.',
'Call without parameters to list available projects.',
'Selection is remembered per session. If your MCP host starts a new session per request, pass a projectKey argument to each tool instead.',
'Do not automatically select a project, ask the user which project they want to select.',
'Returns the current project, its environments, and SDK keys.',
'Include dashboard link in the response.',
Expand Down
42 changes: 18 additions & 24 deletions src/mcp/tools/featureTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
} from '../types'
import { IDevCycleApiClient } from '../api/interface'
import { DevCycleMCPServerInstance } from '../server'
import { handleZodiosValidationErrors } from '../utils/api'
import {
handleZodiosValidationErrors,
omitProjectKey,
MISSING_PROJECT_KEY_ERROR,
} from '../utils/api'
import { dashboardLinks } from '../utils/dashboardLinks'
import { fetchAiPromptsAndRules } from '../utils/github'
import { CleanupFeatureArgsSchema } from '../types'
Expand All @@ -33,12 +37,11 @@ export async function listFeaturesHandler(
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
return await handleZodiosValidationErrors(
() => fetchFeatures(authToken, projectKey, args),
() =>
fetchFeatures(authToken, projectKey, omitProjectKey(args)),
'listFeatures',
)
},
Expand All @@ -59,12 +62,11 @@ export async function createFeatureHandler(
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
return await handleZodiosValidationErrors(
() => createFeature(authToken, projectKey, args),
() =>
createFeature(authToken, projectKey, omitProjectKey(args)),
'createFeature',
)
},
Expand All @@ -82,16 +84,14 @@ export async function updateFeatureHandler(
args: z.infer<typeof UpdateFeatureArgsSchema>,
apiClient: IDevCycleApiClient,
) {
const { key, ...updateData } = args
const { key, ...updateData } = omitProjectKey(args)

return await apiClient.executeWithDashboardLink(
'updateFeature',
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
return await handleZodiosValidationErrors(
() => updateFeature(authToken, projectKey, key, updateData),
Expand All @@ -112,16 +112,14 @@ export async function updateFeatureStatusHandler(
args: z.infer<typeof UpdateFeatureStatusArgsSchema>,
apiClient: IDevCycleApiClient,
) {
const { key, ...statusData } = args
const { key, ...statusData } = omitProjectKey(args)

return await apiClient.executeWithDashboardLink(
'updateFeatureStatus',
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
return await handleZodiosValidationErrors(
() =>
Expand All @@ -148,9 +146,7 @@ export async function deleteFeatureHandler(
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
await handleZodiosValidationErrors(
() => deleteFeature(authToken, projectKey, args.key),
Expand All @@ -173,11 +169,9 @@ export async function getFeatureAuditLogHistoryHandler(
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
const { feature_key, ...auditLogOptions } = args
const { feature_key, ...auditLogOptions } = omitProjectKey(args)
return await handleZodiosValidationErrors(
() =>
getFeatureAuditLogHistory(
Expand Down
1 change: 1 addition & 0 deletions src/mcp/tools/localProjectTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function registerLocalProjectTools(
description: [
'Select a project to use for subsequent MCP operations.',
'Call without parameters to list available projects.',
'Selection is remembered per session. If your MCP host starts a new session per request, pass a projectKey argument to each tool instead.',
'Do not automatically select a project, ask the user which project they want to select.',
'This will update your local DevCycle configuration for the MCP and CLI (~/.config/devcycle/user.yml).',
'Returns the current project, its environments, and SDK keys.',
Expand Down
29 changes: 18 additions & 11 deletions src/mcp/tools/projectTools.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { handleZodiosValidationErrors } from '../utils/api'
import { z } from 'zod'
import {
handleZodiosValidationErrors,
MISSING_PROJECT_KEY_ERROR,
} from '../utils/api'
import { ProjectScopedArgsSchema } from '../types'
import { fetchProject } from '../../api/projects'
import { fetchEnvironments } from '../../api/environments'
import { IDevCycleApiClient } from '../api/interface'
import { DevCycleMCPServerInstance } from '../server'
import { formatProjectWithEnvironments } from '../utils/projectFormatting'
import { dashboardLinks } from '../utils/dashboardLinks'

export async function getCurrentProjectHandler(apiClient: IDevCycleApiClient) {
export async function getCurrentProjectHandler(
args: z.infer<typeof ProjectScopedArgsSchema>,
apiClient: IDevCycleApiClient,
) {
return await apiClient.executeWithDashboardLink(
'getCurrentProject',
null,
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for getting current project. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}

// Fetch the current project details
Expand Down Expand Up @@ -48,19 +54,20 @@ export function registerProjectTools(
'get_current_project',
{
description: [
'Get the currently selected project.',
'Only call this tool if you have already selected a project using the select_project tool.',
'Get a project, defaulting to the currently selected one.',
'Only call this tool if you have already selected a project using the select_project tool, or you are passing a projectKey.',
'Include dashboard link in the response.',
'Returns the current project, its environments, and SDK keys.',
].join('\n'),
annotations: {
title: 'Get Current Project',
readOnlyHint: true,
},
inputSchema: {}, // No parameters needed
inputSchema: ProjectScopedArgsSchema.shape,
},
async () => {
return await getCurrentProjectHandler(apiClient)
async (args: unknown) => {
const validatedArgs = ProjectScopedArgsSchema.parse(args)
return await getCurrentProjectHandler(validatedArgs, apiClient)
},
)
}
23 changes: 14 additions & 9 deletions src/mcp/tools/resultsTools.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { z } from 'zod'
import { handleZodiosValidationErrors } from '../utils/api'
import {
handleZodiosValidationErrors,
omitProjectKey,
MISSING_PROJECT_KEY_ERROR,
} from '../utils/api'
import {
fetchFeatureTotalEvaluations,
fetchProjectTotalEvaluations,
Expand All @@ -22,11 +26,9 @@ export async function getFeatureTotalEvaluationsHandler(
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
const { featureKey, ...apiQueries } = args
const { featureKey, ...apiQueries } = omitProjectKey(args)

return await handleZodiosValidationErrors(
() =>
Expand Down Expand Up @@ -57,12 +59,15 @@ export async function getProjectTotalEvaluationsHandler(
args,
async (authToken: string, projectKey: string | undefined) => {
if (!projectKey) {
throw new Error(
'Project key is required for this operation. Please select a project using the select_project tool first.',
)
throw new Error(MISSING_PROJECT_KEY_ERROR)
}
return await handleZodiosValidationErrors(
() => fetchProjectTotalEvaluations(authToken, projectKey, args),
() =>
fetchProjectTotalEvaluations(
authToken,
projectKey,
omitProjectKey(args),
),
'fetchProjectTotalEvaluations',
)
},
Expand Down
Loading
Loading