Skip to content
Open
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
148 changes: 145 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,44 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

import { loadConfig } from './config.js';
import { UnleashClient } from './unleash/client.js';
import { ServerContext, createLogger, handleToolError } from './context.js';
import {
buildFeatureDevelopmentWorkflowDocument,
featureDevelopmentWorkflowResource,
isFeatureDevelopmentWorkflowUri,
} from './resources/featureDevelopmentWorkflow.js';
import {
buildLocalChangeChecklistDocument,
isLocalChangeChecklistUri,
localChangeChecklistResource,
} from './resources/localChangeChecklist.js';
import {
buildWorkspaceSummaryDocument,
isWorkspaceSummaryUri,
workspaceSummaryResource,
} from './resources/workspaceSummary.js';
import {
backendGuardrailsResource,
buildBackendGuardrailsDocument,
isBackendGuardrailsUri,
} from './resources/backendGuardrails.js';
import { createFlag, createFlagTool } from './tools/createFlag.js';
import { evaluateChange, evaluateChangeTool } from './tools/evaluateChange.js';
import { wrapChange, wrapChangeTool } from './tools/wrapChange.js';
import {
prepareLocalChange,
prepareLocalChangeTool,
} from './tools/prepareLocalChange.js';
import { decideLocalFlow, decideLocalFlowTool } from './tools/decideLocalFlow.js';
import { applyPatch, applyPatchTool } from './tools/applyPatch.js';
import { runChecks, runChecksTool } from './tools/runChecks.js';

/**
* Main entry point for the MCP server.
Expand All @@ -60,15 +89,28 @@ async function main(): Promise<void> {
config.server.dryRun
);

const instructions = [
'Local-change policy:',
'1) When unsure, call decide_local_flow to confirm this is a local code change.',
'2) Always call prepare_local_change before editing files in this repository.',
'3) If the task is risky, user-facing, or mentions feature flags, call evaluate_change.',
'4) Use wrap_change when guarding code with an Unleash flag and summarize diffs with apply_patch as needed.',
'5) Before finishing, call run_checks (or run commands manually) to validate formatter, linter, and tests.',
].join('\n');

// Create MCP server
const server = new Server(
{
name: 'unleash-mcp',
version: '0.1.0',
description:
'Purpose-driven Unleash feature flag assistant. Default workflow: decide_local_flow → prepare_local_change → evaluate_change → create_flag → wrap_change → run_checks. Whether you are touching a single file or planning a large refactor, start with prepare_local_change to gather local guardrails, then evaluate_change to score risk and steer next steps.',
},
{
capabilities: {
tools: {},
resources: {},
instructions,
},
}
);
Expand All @@ -84,7 +126,15 @@ async function main(): Promise<void> {
// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [createFlagTool, evaluateChangeTool, wrapChangeTool],
tools: [
decideLocalFlowTool,
prepareLocalChangeTool,
evaluateChangeTool,
createFlagTool,
wrapChangeTool,
applyPatchTool,
runChecksTool,
],
};
});

Expand All @@ -95,15 +145,27 @@ async function main(): Promise<void> {
logger.debug(`Tool called: ${name}`, args);

switch (name) {
case 'create_flag':
return await createFlag(context, args, request.params._meta?.progressToken);
case 'decide_local_flow':
return await decideLocalFlow(context, args);

case 'prepare_local_change':
return await prepareLocalChange(context, args);

case 'evaluate_change':
return await evaluateChange(context, args);

case 'create_flag':
return await createFlag(context, args, request.params._meta?.progressToken);

case 'wrap_change':
return await wrapChange(context, args);

case 'apply_patch':
return await applyPatch(context, args);

case 'run_checks':
return await runChecks(context, args);

default:
throw new Error(`Unknown tool: ${name}`);
}
Expand All @@ -113,6 +175,86 @@ async function main(): Promise<void> {
}
});

// Register proactive guidance resource handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
workspaceSummaryResource,
backendGuardrailsResource,
localChangeChecklistResource,
featureDevelopmentWorkflowResource,
],
};
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;

if (isWorkspaceSummaryUri(uri)) {
logger.debug(`Reading resource: ${uri}`);
return {
contents: [
{
uri,
mimeType: workspaceSummaryResource.mimeType,
text: await buildWorkspaceSummaryDocument(),
},
],
};
}

if (isBackendGuardrailsUri(uri)) {
logger.debug(`Reading resource: ${uri}`);
return {
contents: [
{
uri,
mimeType: backendGuardrailsResource.mimeType,
text: buildBackendGuardrailsDocument(),
},
],
};
}

if (isFeatureDevelopmentWorkflowUri(uri)) {
logger.debug(`Reading resource: ${uri}`);
return {
contents: [
{
uri,
mimeType: featureDevelopmentWorkflowResource.mimeType,
text: buildFeatureDevelopmentWorkflowDocument(config),
},
],
};
}

if (isLocalChangeChecklistUri(uri)) {
logger.debug(`Reading resource: ${uri}`);
return {
contents: [
{
uri,
mimeType: localChangeChecklistResource.mimeType,
text: buildLocalChangeChecklistDocument(config),
},
],
};
}

logger.warn(`Unknown resource requested: ${uri}`);

return {
contents: [
{
uri,
mimeType: 'text/plain',
text: `Resource not found: ${uri}. Available resources: unleash://workspace/summary, unleash://guides/backend-guardrails, unleash://guides/local-change-checklist, unleash://guides/feature-development-workflow`,
},
],
};
});

// Start server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
Expand Down
37 changes: 37 additions & 0 deletions src/resources/backendGuardrails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Resource } from '@modelcontextprotocol/sdk/types.js';

const BACKEND_GUARDRAILS_URI = 'unleash://guides/backend-guardrails';

export const backendGuardrailsResource: Resource = {
uri: BACKEND_GUARDRAILS_URI,
name: 'Backend Change Guardrails',
description:
'Server-side checklist for edits in src/lib, src/unleash, or API layers. Highlights when to call prepare_local_change and evaluate_change before touching code.',
mimeType: 'text/markdown',
};

export function buildBackendGuardrailsDocument(): string {
const lines: string[] = [
'# Backend Change Guardrails',
'',
'When working on server-side code (e.g., `src/lib`, `src/unleash`, API controllers, services):',
'',
'1. **Call `prepare_local_change` first.** Capture the task summary and let the MCP list impacted files, repo commands, and guardrails before editing anything.',
'2. **Call `evaluate_change`** with the plan. Backend changes often affect rollout and risk; the tool will tell you if a feature flag is required.',
'3. **Check for existing flags** around the area. If wrapping new logic, plan to reuse or extend those flags before creating a new one.',
'4. **Use `wrap_change`** once a flag is confirmed so the snippet matches the project conventions.',
'5. **Summarize the diff with `apply_patch` and run `run_checks`** (formatter, linter, tests) before finishing.',
'',
'Extra considerations:',
'',
'- Document rollout and cleanup expectations in the change description.',
'- Update or add backend tests to cover enabled/disabled flag paths.',
'- Watch for database or integration side effects that may require gradual rollout.',
];

return lines.join('\n');
}

export function isBackendGuardrailsUri(uri: string): boolean {
return uri === BACKEND_GUARDRAILS_URI;
}
66 changes: 66 additions & 0 deletions src/resources/featureDevelopmentWorkflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Resource } from '@modelcontextprotocol/sdk/types.js';
import { Config } from '../config.js';

const FEATURE_DEVELOPMENT_URI = 'unleash://guides/feature-development-workflow';

export const featureDevelopmentWorkflowResource: Resource = {
uri: FEATURE_DEVELOPMENT_URI,
name: 'Feature Development Workflow',
description:
'Start here when you are implementing a feature or risky change. Explains when to call evaluate_change, create_flag, and wrap_change.',
mimeType: 'text/markdown',
};

export function buildFeatureDevelopmentWorkflowDocument(config: Config): string {
const lines: Array<string | undefined> = [
'# Feature Development with Unleash MCP',
'',
'Use this workflow whenever you begin a product change. It keeps work safely gated behind Unleash feature flags and tells the LLM which tool to call next.',
'',
'> Planning anything from a small safety refactor to a platform-wide initiative? Capture the intent and known touchpoints in **`prepare_local_change`** and **`evaluate_change`** before writing code so you can outline discovery steps, flag needs, and rollout strategy up front—even if diff files are not ready yet.',
'',
'## 1. Assess the change first',
'',
'- Call **`prepare_local_change`** with the task summary to gather guardrails, repo commands, and suggested next steps.',
'- Follow up with **`evaluate_change`** as soon as a new feature or risky modification is mentioned.',
'- Provide repo, branch, files, and any risk notes so the evaluation can score the change.',
'- Follow the JSON result. When it says a flag is needed, move to creation immediately.',
'',
'## 2. Create the flag if required',
'',
'- Use **`create_flag`** with the recommended flag name, type, and description from the evaluation output.',
'- Keep descriptions explicit about rollout intent and clean-up expectations.',
config.unleash.defaultProject
? `- Default project detected: \`${config.unleash.defaultProject}\`. Override only when the work belongs elsewhere.`
: undefined,
config.server.dryRun
? '- Server is running in **dry-run** mode. The tool will validate inputs and log the request without calling Unleash.'
: undefined,
'',
'## 3. Wrap the implementation',
'',
'- Call **`wrap_change`** immediately after flag creation (or when reusing an existing flag).',
'- Provide the target file name and any code context so the tool can detect conventions and suggest matching templates.',
'- Apply the recommended snippet, then test the guarded code path.',
'',
'## 4. Close the loop',
'',
'- Confirm tests or QA plans cover both flag states.',
'- Record rollout decisions (gradual rollout, kill switches, cleanup owner).',
'- If the evaluation reported “no new flag”, document why and proceed without flag creation.',
'',
'### Quick Decision Checklist',
'',
'1. Did we already check for parent flags? → `evaluate_change` helps surface them.',
'2. Do we understand rollout impact? → The evaluation guidance cites Unleash best practices.',
'3. Are we mirroring existing code conventions? → `wrap_change` detects patterns when you pass code context.',
'',
'Keep this workflow in mind: **prepare → evaluate → create → wrap → verify**. The LLM should reach for these tools without waiting for the user to ask.',
];

return lines.filter((line): line is string => Boolean(line)).join('\n');
}

export function isFeatureDevelopmentWorkflowUri(uri: string): boolean {
return uri === FEATURE_DEVELOPMENT_URI;
}
47 changes: 47 additions & 0 deletions src/resources/localChangeChecklist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Resource } from '@modelcontextprotocol/sdk/types.js';
import { Config } from '../config.js';

const LOCAL_CHANGE_CHECKLIST_URI = 'unleash://guides/local-change-checklist';

export const localChangeChecklistResource: Resource = {
uri: LOCAL_CHANGE_CHECKLIST_URI,
name: 'Local Change Checklist',
description:
'Checklist for preparing local modifications. Highlights prepare_local_change, evaluate_change, and repo guardrails to keep work safe.',
mimeType: 'text/markdown',
};

export function buildLocalChangeChecklistDocument(config: Config): string {
const lines: string[] = [
'# Local Change Checklist',
'',
'Follow this sequence before touching code:',
'',
'0. If you are unsure whether this is a local code change, call **`decide_local_flow`**.',
'1. **Call `prepare_local_change`** with the task summary to collect guardrails, suggested files, and test commands.',
'2. Review the output and open any referenced resources (project guardrails, documentation links).',
'3. **Call `evaluate_change`** with the planned work (even if you only have a proposal) to assess flag needs and risk.',
'4. If a flag is required, **call `create_flag`**, then **`wrap_change`** to get language-specific snippets.',
'5. Implement the smallest safe diff, run the recommended checks, and capture follow-up tasks.',
'',
'## Quick Hints',
'',
'- Keep scopes tight; prefer iterative changes gated behind the new flag.',
'- Surface risky or high-impact areas (auth, payments, migrations) in the tool inputs.',
'- Note cleanup expectations so rollout is planned from the start.',
'- Use `unleash://workspace/summary` to recall formatter/linter/test commands quickly.',
'',
config.unleash.defaultProject
? `Default project: \`${config.unleash.defaultProject}\``
: 'Set `UNLEASH_DEFAULT_PROJECT` to avoid passing projectId manually.',
config.server.dryRun
? 'Running in **dry-run** mode: Unleash API calls are validated but not executed.'
: undefined,
].filter((line): line is string => Boolean(line));

return lines.join('\n');
}

export function isLocalChangeChecklistUri(uri: string): boolean {
return uri === LOCAL_CHANGE_CHECKLIST_URI;
}
Loading