/**
* Integration test for MCP Resources registration and handler behavior.
*
* Tests that:
* 1. Prompt resources register correctly and return expected data
* 2. Gate resources register correctly and return expected data
* 3. Resource list changed notifications work
* 4. Resources work with real prompt/gate data
*/
import { describe, test, expect, beforeAll, afterAll, jest, beforeEach } from '@jest/globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { registerResources, notifyResourcesChanged } from '../../../src/modules/resources/index.js';
import type { ResourceDependencies } from '../../../src/modules/resources/types.js';
import { RESOURCE_URI_PATTERNS } from '../../../src/modules/resources/types.js';
import type { Logger } from '../../../src/infra/logging/index.js';
import { GateManager, createGateManager } from '../../../src/engine/gates/gate-manager.js';
// Set up server root for gate loader to find resources
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const serverRoot = path.resolve(__dirname, '../../..');
describe('MCP Resources Registration Integration', () => {
let gateManager: GateManager;
let mockLogger: Logger;
let originalServerRoot: string | undefined;
// Mock MCP server with captured registrations
let registeredResources: Map<
string,
{
template: unknown;
options: unknown;
readHandler: (uri: URL, variables: Record<string, string>) => Promise<unknown>;
}
>;
let mockMcpServer: {
registerResource: jest.Mock;
sendResourceListChanged: jest.Mock;
};
beforeAll(async () => {
// Save and set MCP_SERVER_ROOT for gate loader
originalServerRoot = process.env.MCP_SERVER_ROOT;
process.env.MCP_SERVER_ROOT = serverRoot;
mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
};
gateManager = await createGateManager(mockLogger, { debug: false });
});
beforeEach(() => {
// Reset registered resources for each test
registeredResources = new Map();
// Create mock MCP server that captures registrations
mockMcpServer = {
registerResource: jest.fn(
(name: string, template: unknown, options: unknown, readHandler: unknown) => {
registeredResources.set(name, {
template,
options,
readHandler: readHandler as (
uri: URL,
variables: Record<string, string>
) => Promise<unknown>,
});
}
),
sendResourceListChanged: jest.fn(),
};
});
afterAll(() => {
// Restore original MCP_SERVER_ROOT
if (originalServerRoot !== undefined) {
process.env.MCP_SERVER_ROOT = originalServerRoot;
} else {
delete process.env.MCP_SERVER_ROOT;
}
});
describe('registerResources()', () => {
test('registers prompt resources when prompt assets are provided', () => {
const mockPromptAssetManager = {
getConvertedPrompts: () => [
{
id: 'test-prompt',
name: 'Test Prompt',
description: 'A test prompt',
category: 'testing',
userMessageTemplate: 'Hello {{name}}',
arguments: [{ name: 'name', type: 'string', required: true }],
},
],
};
const dependencies: ResourceDependencies = {
logger: mockLogger,
promptManager: mockPromptAssetManager,
};
registerResources(mockMcpServer as never, dependencies);
expect(mockMcpServer.registerResource).toHaveBeenCalled();
expect(registeredResources.has('prompts')).toBe(true);
expect(registeredResources.has('prompt')).toBe(true);
expect(registeredResources.has('prompt-template')).toBe(true);
});
test('registers gate resources when gateManager is provided', () => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
gateManager: {
list: (enabledOnly?: boolean) => gateManager.list(enabledOnly),
get: (id: string) => gateManager.get(id),
},
};
registerResources(mockMcpServer as never, dependencies);
expect(registeredResources.has('gates')).toBe(true);
expect(registeredResources.has('gate')).toBe(true);
expect(registeredResources.has('gate-guidance')).toBe(true);
});
test('skips prompt resources when prompt assets not provided', () => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
// No prompt assets
};
registerResources(mockMcpServer as never, dependencies);
expect(registeredResources.has('prompts')).toBe(false);
expect(registeredResources.has('prompt')).toBe(false);
expect(mockLogger.warn).toHaveBeenCalled();
});
test('skips gate resources when gateManager not provided', () => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
// No gateManager
};
registerResources(mockMcpServer as never, dependencies);
expect(registeredResources.has('gates')).toBe(false);
expect(registeredResources.has('gate')).toBe(false);
expect(mockLogger.warn).toHaveBeenCalled();
});
});
describe('Prompt Resource Handlers', () => {
const testPrompts = [
{
id: 'analysis-report',
name: 'Analysis Report',
description: 'Generate detailed analysis reports',
category: 'analysis',
userMessageTemplate: 'Analyze {{topic}} with focus on {{aspect}}',
arguments: [
{ name: 'topic', type: 'string', required: true, description: 'Topic to analyze' },
{ name: 'aspect', type: 'string', required: false },
],
chainSteps: [
{ promptId: 'gather-data', stepName: 'Gather Data' },
{ promptId: 'synthesize', stepName: 'Synthesize Results' },
],
},
{
id: 'simple-greeting',
name: 'Simple Greeting',
description: 'A simple greeting prompt',
category: 'greeting',
userMessageTemplate: 'Hello, {{name}}!',
arguments: [{ name: 'name', type: 'string', required: true }],
},
];
beforeEach(() => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
promptManager: {
getConvertedPrompts: () => testPrompts,
},
};
registerResources(mockMcpServer as never, dependencies);
});
test('prompts list handler returns all prompts with metadata', async () => {
const handler = registeredResources.get('prompts')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.PROMPT_LIST}`),
{}
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('text/plain');
// New compact format: "Prompts (N):\nid: title - args"
const text = result.contents[0].text;
expect(text).toContain('Prompts (2):');
expect(text).toContain('analysis-report: Analysis Report - topic, aspect');
expect(text).toContain('simple-greeting: Simple Greeting - name');
});
test('individual prompt handler returns full content', async () => {
const handler = registeredResources.get('prompt')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.PROMPT_ITEM.replace('{id}', 'analysis-report')}`
),
{ id: 'analysis-report' }
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('text/markdown');
const content = result.contents[0].text;
expect(content).toContain('# Analysis Report');
expect(content).toContain('**ID:** `analysis-report`');
expect(content).toContain('**Category:** analysis');
expect(content).toContain('**Type:** chain');
expect(content).toContain('## Arguments');
expect(content).toContain('**topic** (required)');
expect(content).toContain('## Chain Steps');
expect(content).toContain('2 steps: Gather Data → Synthesize Results');
expect(content).toContain('## Template');
});
test('prompt template handler returns raw template only', async () => {
const handler = registeredResources.get('prompt-template')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.PROMPT_TEMPLATE.replace('{id}', 'simple-greeting')}`
),
{ id: 'simple-greeting' }
)) as { contents: Array<{ text: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toBe('Hello, {{name}}!');
});
test('prompt handler throws for non-existent prompt', async () => {
const handler = registeredResources.get('prompt')?.readHandler;
await expect(
handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.PROMPT_ITEM.replace('{id}', 'non-existent')}`
),
{ id: 'non-existent' }
)
).rejects.toThrow('Prompt not found: non-existent');
});
});
describe('Gate Resource Handlers', () => {
beforeEach(() => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
gateManager: {
list: (enabledOnly?: boolean) => gateManager.list(enabledOnly),
get: (id: string) => gateManager.get(id),
},
};
registerResources(mockMcpServer as never, dependencies);
});
test('gates list handler returns all gates with metadata', async () => {
const handler = registeredResources.get('gates')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.GATE_LIST}`),
{}
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('text/plain');
// New compact format: "Gates (N):\nid: name (type)"
const text = result.contents[0].text;
expect(text).toMatch(/^Gates \(\d+\):/);
// Should have gates loaded from server/gates/
expect(text).toContain('code-quality:');
expect(text).toContain('(validation)');
});
test('individual gate handler returns full content', async () => {
const handler = registeredResources.get('gate')?.readHandler;
expect(handler).toBeDefined();
// Use a known gate from the codebase
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.GATE_ITEM.replace('{id}', 'code-quality')}`),
{ id: 'code-quality' }
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('text/markdown');
const content = result.contents[0].text;
expect(content).toContain('**ID:** `code-quality`');
expect(content).toContain('**Type:**');
expect(content).toContain('**Severity:**');
});
test('gate guidance handler returns raw guidance content', async () => {
const handler = registeredResources.get('gate-guidance')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.GATE_GUIDANCE.replace('{id}', 'code-quality')}`
),
{ id: 'code-quality' }
)) as { contents: Array<{ text: string }> };
expect(result.contents).toHaveLength(1);
// Should have guidance content (or placeholder if none)
expect(typeof result.contents[0].text).toBe('string');
});
test('gate handler throws for non-existent gate', async () => {
const handler = registeredResources.get('gate')?.readHandler;
await expect(
handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.GATE_ITEM.replace('{id}', 'non-existent-gate')}`
),
{ id: 'non-existent-gate' }
)
).rejects.toThrow('Gate not found: non-existent-gate');
});
});
describe('notifyResourcesChanged()', () => {
test('sends resource list changed notification', () => {
notifyResourcesChanged(mockMcpServer as never, mockLogger);
expect(mockMcpServer.sendResourceListChanged).toHaveBeenCalledTimes(1);
expect(mockLogger.debug).toHaveBeenCalled();
});
test('handles notification failure gracefully', () => {
mockMcpServer.sendResourceListChanged.mockImplementation(() => {
throw new Error('Connection closed');
});
// Should not throw
expect(() => {
notifyResourcesChanged(mockMcpServer as never, mockLogger);
}).not.toThrow();
expect(mockLogger.warn).toHaveBeenCalled();
});
});
describe('Methodology Resource Handlers', () => {
const testFrameworks = [
{
id: 'CAGEERF',
name: 'CAGEERF Framework',
description: 'Context, Analysis, Goals, Execution, Evaluation, Refinement methodology',
type: 'methodology',
systemPromptTemplate: 'Apply the CAGEERF methodology systematically...',
executionGuidelines: ['Start with context', 'Analyze thoroughly', 'Set clear goals'],
priority: 100,
enabled: true,
},
{
id: 'ReACT',
name: 'ReACT Framework',
description: 'Reason, Act, Observe loop for problem solving',
type: 'methodology',
systemPromptTemplate: 'Use the ReACT loop...',
executionGuidelines: ['Reason first', 'Take action', 'Observe results'],
priority: 90,
enabled: false,
},
];
beforeEach(() => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
frameworkManager: {
listFrameworks: (enabledOnly?: boolean) =>
enabledOnly ? testFrameworks.filter((f) => f.enabled) : testFrameworks,
getFramework: (id: string) => testFrameworks.find((f) => f.id === id),
},
};
registerResources(mockMcpServer as never, dependencies);
});
test('registers methodology resources when frameworkManager is provided', () => {
expect(registeredResources.has('methodologies')).toBe(true);
expect(registeredResources.has('methodology')).toBe(true);
expect(registeredResources.has('methodology-system-prompt')).toBe(true);
});
test('methodologies list handler returns all methodologies with metadata', async () => {
const handler = registeredResources.get('methodologies')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.METHODOLOGY_LIST}`),
{}
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('text/plain');
// New compact format: "Methodologies (N):\nid: name [disabled]"
const text = result.contents[0].text;
expect(text).toContain('Methodologies (2):');
expect(text).toContain('CAGEERF: CAGEERF Framework');
expect(text).toContain('ReACT: ReACT Framework [disabled]');
});
test('individual methodology handler returns full content', async () => {
const handler = registeredResources.get('methodology')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.METHODOLOGY_ITEM.replace('{id}', 'CAGEERF')}`),
{ id: 'CAGEERF' }
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('text/markdown');
const content = result.contents[0].text;
expect(content).toContain('# CAGEERF Framework');
expect(content).toContain('**ID:** `CAGEERF`');
expect(content).toContain('**Type:** methodology');
expect(content).toContain('**Priority:** 100');
expect(content).toContain('**Enabled:** Yes');
expect(content).toContain('## Execution Guidelines');
expect(content).toContain('- Start with context');
expect(content).toContain('## System Prompt Template');
});
test('methodology system prompt handler returns raw template only', async () => {
const handler = registeredResources.get('methodology-system-prompt')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.METHODOLOGY_SYSTEM_PROMPT.replace('{id}', 'CAGEERF')}`
),
{ id: 'CAGEERF' }
)) as { contents: Array<{ text: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toBe('Apply the CAGEERF methodology systematically...');
});
test('methodology handler throws for non-existent methodology', async () => {
const handler = registeredResources.get('methodology')?.readHandler;
await expect(
handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.METHODOLOGY_ITEM.replace('{id}', 'non-existent')}`
),
{ id: 'non-existent' }
)
).rejects.toThrow('Methodology not found: non-existent');
});
});
describe('Observability Resource Handlers', () => {
const testSessions = [
{
sessionId: 'session-123',
chainId: 'chain-abc',
currentStep: 2,
totalSteps: 5,
pendingReview: false,
lastActivity: Date.now() - 1000,
startTime: Date.now() - 60000,
promptName: 'Analysis Chain',
promptId: 'analysis-chain',
},
{
sessionId: 'session-456',
chainId: 'chain-def',
currentStep: 1,
totalSteps: 3,
pendingReview: true,
lastActivity: Date.now() - 5000,
startTime: Date.now() - 120000,
promptName: 'Review Chain',
},
];
const testSessionDetails = {
sessionId: 'session-123',
chainId: 'chain-abc',
state: {
currentStep: 2,
totalSteps: 5,
stepStates: new Map<number, unknown>([[1, { status: 'completed' }]]),
},
startTime: Date.now() - 60000,
lastActivity: Date.now() - 1000,
originalArgs: { topic: 'testing' },
pendingGateReview: undefined,
};
const testMetrics = {
executionStats: {
totalExecutions: 100,
successfulExecutions: 95,
failedExecutions: 5,
averageExecutionTime: 250,
},
systemMetrics: {
uptime: 3600000,
memoryUsage: { heapUsed: 50000000, heapTotal: 100000000 },
averageResponseTime: 150,
requestsPerMinute: 10,
errorRate: 0.05,
},
};
const testSessionStats = {
totalActiveSessions: 2,
totalCompletedSessions: 50,
sessionsWithPendingReviews: 1,
};
beforeEach(() => {
const dependencies: ResourceDependencies = {
logger: mockLogger,
chainSessionManager: {
listActiveSessions: (limit?: number) => testSessions.slice(0, limit),
getSession: (sessionId: string) =>
sessionId === 'session-123' ? testSessionDetails : undefined,
getSessionStats: () => testSessionStats,
},
metricsCollector: {
getAnalyticsSummary: () => testMetrics,
},
};
registerResources(mockMcpServer as never, dependencies);
});
test('registers session resources when chainSessionManager is provided', () => {
expect(registeredResources.has('sessions')).toBe(true);
expect(registeredResources.has('session')).toBe(true);
});
test('registers metrics resources when metricsCollector is provided', () => {
expect(registeredResources.has('metrics-pipeline')).toBe(true);
});
test('sessions list handler returns active sessions with metadata', async () => {
const handler = registeredResources.get('sessions')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.SESSION_LIST}`),
{}
)) as { contents: Array<{ text: string }> };
expect(result.contents).toHaveLength(1);
const parsed = JSON.parse(result.contents[0].text);
expect(parsed).toHaveLength(2);
expect(parsed[0].name).toBe('session-123');
expect(parsed[0].chainId).toBe('chain-abc');
expect(parsed[0].currentStep).toBe(2);
expect(parsed[0].totalSteps).toBe(5);
expect(parsed[0].pendingReview).toBe(false);
expect(parsed[1].pendingReview).toBe(true);
});
test('individual session handler returns full state', async () => {
const handler = registeredResources.get('session')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.SESSION_ITEM.replace('{chainId}', 'session-123')}`
),
{ chainId: 'session-123' }
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('application/json');
const parsed = JSON.parse(result.contents[0].text);
expect(parsed.sessionId).toBe('session-123');
expect(parsed.chainId).toBe('chain-abc');
expect(parsed.progress.currentStep).toBe(2);
expect(parsed.progress.totalSteps).toBe(5);
expect(parsed.progress.percentComplete).toBe(40);
expect(parsed.originalArgs.topic).toBe('testing');
expect(parsed.hasPendingReview).toBe(false);
});
test('session handler throws for non-existent session', async () => {
const handler = registeredResources.get('session')?.readHandler;
await expect(
handler!(
new URL(
`resource://${RESOURCE_URI_PATTERNS.SESSION_ITEM.replace('{chainId}', 'non-existent')}`
),
{ chainId: 'non-existent' }
)
).rejects.toThrow('Session not found: non-existent');
});
test('metrics pipeline handler returns analytics summary', async () => {
const handler = registeredResources.get('metrics-pipeline')?.readHandler;
expect(handler).toBeDefined();
const result = (await handler!(
new URL(`resource://${RESOURCE_URI_PATTERNS.METRICS_PIPELINE}`),
{}
)) as { contents: Array<{ text: string; mimeType: string }> };
expect(result.contents).toHaveLength(1);
expect(result.contents[0].mimeType).toBe('application/json');
const parsed = JSON.parse(result.contents[0].text);
expect(parsed.execution.totalExecutions).toBe(100);
expect(parsed.execution.successfulExecutions).toBe(95);
expect(parsed.system.uptime).toBe(3600000);
expect(parsed.sessions.totalActiveSessions).toBe(2);
expect(parsed.sessions.sessionsWithPendingReviews).toBe(1);
expect(parsed.timestamp).toBeDefined();
});
});
describe('URI Pattern Constants', () => {
test('prompt patterns include full URI scheme', () => {
expect(RESOURCE_URI_PATTERNS.PROMPT_LIST).toBe('resource://prompt/');
expect(RESOURCE_URI_PATTERNS.PROMPT_ITEM).toBe('resource://prompt/{id}');
expect(RESOURCE_URI_PATTERNS.PROMPT_TEMPLATE).toBe('resource://prompt/{id}/template');
});
test('gate patterns include full URI scheme', () => {
expect(RESOURCE_URI_PATTERNS.GATE_LIST).toBe('resource://gate/');
expect(RESOURCE_URI_PATTERNS.GATE_ITEM).toBe('resource://gate/{id}');
expect(RESOURCE_URI_PATTERNS.GATE_GUIDANCE).toBe('resource://gate/{id}/guidance');
});
test('methodology patterns include full URI scheme', () => {
expect(RESOURCE_URI_PATTERNS.METHODOLOGY_LIST).toBe('resource://methodology/');
expect(RESOURCE_URI_PATTERNS.METHODOLOGY_ITEM).toBe('resource://methodology/{id}');
expect(RESOURCE_URI_PATTERNS.METHODOLOGY_SYSTEM_PROMPT).toBe(
'resource://methodology/{id}/system-prompt'
);
});
test('observability patterns include full URI scheme', () => {
expect(RESOURCE_URI_PATTERNS.SESSION_LIST).toBe('resource://session/');
expect(RESOURCE_URI_PATTERNS.SESSION_ITEM).toBe('resource://session/{chainId}');
expect(RESOURCE_URI_PATTERNS.METRICS_PIPELINE).toBe('resource://metrics/pipeline');
});
});
});