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
83 changes: 45 additions & 38 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,49 +201,56 @@ Resources can reuse existing tool logic for consistency:

```typescript
// src/mcp/resources/some_resource.ts
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
import { getSomeResourceLogic } from '../tools/some-workflow/get_some_resource.js';

// Testable resource logic separated from MCP handler
export async function someResourceResourceLogic(
executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<{ contents: Array<{ text: string }> }> {
try {
log('info', 'Processing some resource request');

const result = await getSomeResourceLogic({}, executor);

if (result.isError) {
const errorText = result.content[0]?.text;
throw new Error(
typeof errorText === 'string' ? errorText : 'Failed to retrieve some resource data',
);
}

return {
contents: [
{
text:
typeof result.content[0]?.text === 'string'
? result.content[0].text
: 'No data for that resource is available',
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error in some_resource resource handler: ${errorMessage}`);

return {
contents: [
{
text: `Error retrieving resource data: ${errorMessage}`,
},
],
};
}
}

export default {
uri: 'xcodebuildmcp://some_resource',
name: 'some_resource',
description: 'Returns some resource information',
mimeType: 'text/plain',
async handler(
uri: URL,
executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<{ contents: Array<{ text: string }> }> {
try {
log('info', 'Processing simulators resource request');

const result = await getSomeResource({}, executor);

if (result.isError) {
const errorText = result.content[0]?.text;
throw new Error(
typeof errorText === 'string' ? errorText : 'Failed to retrieve some resource data',
);
}

return {
contents: [
{
text:
typeof result.content[0]?.text === 'string'
? result.content[0].text
: 'No data for that resource is available',
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error in some_resource resource handler: ${errorMessage}`);

return {
contents: [
{
text: `Error retrieving resource data: ${errorMessage}`,
},
],
};
}
async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
return someResourceResourceLogic();
},
};
```
Expand Down
106 changes: 83 additions & 23 deletions docs/PLUGIN_DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,19 +304,47 @@ Resources are located in `src/resources/` and follow this pattern:

```typescript
// src/resources/example.ts
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';

// Testable resource logic separated from MCP handler
export async function exampleResourceLogic(
executor: CommandExecutor,
): Promise<{ contents: Array<{ text: string }> }> {
try {
log('info', 'Processing example resource request');

// Use the executor to get data
const result = await executor(['some', 'command'], 'Example Resource Operation');

if (!result.success) {
throw new Error(result.error || 'Failed to get resource data');
}

return {
contents: [{ text: result.output || 'resource data' }]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error in example resource handler: ${errorMessage}`);

return {
contents: [
{
text: `Error retrieving resource data: ${errorMessage}`,
},
],
};
}
}

export default {
uri: 'xcodebuildmcp://example',
name: 'example'
name: 'example',
description: 'Description of the resource data',
mimeType: 'text/plain',
async handler(
executor: CommandExecutor = getDefaultCommandExecutor()
): Promise<{ contents: Array<{ text: string }> }> {
// Resource implementation
return {
contents: [{ text: 'resource data' }]
};
}
async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
return exampleResourceLogic(getDefaultCommandExecutor());
},
};
```

Expand All @@ -325,17 +353,16 @@ export default {
**Reuse Existing Logic**: Resources that mirror tools should reuse existing tool logic for consistency:

```typescript
// src/mcp/resources/simulators.ts (simplified exmaple)
import { list_simsLogic } from '../plugins/simulator-shared/list_sims.js';
// src/mcp/resources/simulators.ts (simplified example)
import { list_simsLogic } from '../tools/simulator-shared/list_sims.js';

export default {
uri: 'xcodebuildmcp://simulators',
name: 'simulators'
description: 'Available iOS simulators with UUIDs and states',
mimeType: 'text/plain',
async handler(
executor: CommandExecutor = getDefaultCommandExecutor()
): Promise<{ contents: Array<{ text: string }> }> {
async handler(uri: URL): Promise<{ contents: Array<{ text: string }> }> {
const executor = getDefaultCommandExecutor();
const result = await list_simsLogic({}, executor);
return {
contents: [{ text: result.content[0].text }]
Expand All @@ -352,19 +379,52 @@ Create tests in `src/mcp/resources/__tests__/`:

```typescript
// src/mcp/resources/__tests__/example.test.ts
import exampleResource from '../example.js';
import exampleResource, { exampleResourceLogic } from '../example.js';
import { createMockExecutor } from '../../utils/test-common.js';

describe('example resource', () => {
it('should return resource data', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'test data'
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(exampleResource.uri).toBe('xcodebuildmcp://example');
});

it('should export correct description', () => {
expect(exampleResource.description).toBe('Description of the resource data');
});

it('should export correct mimeType', () => {
expect(exampleResource.mimeType).toBe('text/plain');
});

it('should export handler function', () => {
expect(typeof exampleResource.handler).toBe('function');
});
});

describe('Resource Logic Functionality', () => {
it('should return resource data successfully', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'test data'
});

// Test the logic function directly, not the handler
const result = await exampleResourceLogic(mockExecutor);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('expected data');
});

it('should handle command execution errors', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Command failed'
});

const result = await exampleResourceLogic(mockExecutor);

expect(result.contents[0].text).toContain('Error retrieving');
});

const result = await exampleResource.handler(mockExecutor);

expect(result.contents[0].text).toContain('expected data');
});
});
```
Expand Down
30 changes: 15 additions & 15 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,22 @@ Test → Plugin Handler → utilities → [DEPENDENCY INJECTION] createMockExecu
All plugin handlers must support dependency injection:

```typescript
export function tool_nameLogic(
args: Record<string, unknown>,
commandExecutor: CommandExecutor,
fileSystemExecutor?: FileSystemExecutor
): Promise<ToolResponse> {
// Use injected executors
const result = await executeCommand(['xcrun', 'simctl', 'list'], commandExecutor);
return createTextResponse(result.output);
}

export default {
name: 'tool_name',
description: 'Tool description',
schema: { /* zod schema */ },
async handler(
args: Record<string, unknown>,
commandExecutor: CommandExecutor = getDefaultCommandExecutor(),
fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor()
): Promise<ToolResponse> {
// Use injected executors
const result = await executeCommand(['xcrun', 'simctl', 'list'], commandExecutor);
return createTextResponse(result.output);
async handler(args: Record<string, unknown>): Promise<ToolResponse> {
return tool_nameLogic(args, getDefaultCommandExecutor(), getDefaultFileSystemExecutor());
},
};
```
Expand All @@ -128,7 +132,7 @@ it('should handle successful command execution', async () => {
output: 'BUILD SUCCEEDED'
});

const result = await tool.handler(
const result = await tool_nameLogic(
{ projectPath: '/test.xcodeproj', scheme: 'MyApp' },
mockExecutor
);
Expand Down Expand Up @@ -509,12 +513,8 @@ const result = await tool.handler(params, mockCmd, mockFS);
**Fix**: Update handler signature:

```typescript
async handler(
args: Record<string, unknown>,
commandExecutor: CommandExecutor = getDefaultCommandExecutor(),
fileSystemExecutor: FileSystemExecutor = getDefaultFileSystemExecutor()
): Promise<ToolResponse> {
// Use injected executors
async handler(args: Record<string, unknown>): Promise<ToolResponse> {
return tool_nameLogic(args, getDefaultCommandExecutor(), getDefaultFileSystemExecutor());
}
```

Expand Down
Loading