Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
109 changes: 109 additions & 0 deletions src/mcp/resources/__tests__/devices.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, it, expect } from 'vitest';

import devicesResource from '../devices.js';
import { createMockExecutor } from '../../../utils/command.js';

describe('devices resource', () => {
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(devicesResource.uri).toBe('xcodebuildmcp://devices');
});

it('should export correct description', () => {
expect(devicesResource.description).toBe(
'Connected physical Apple devices with their UUIDs, names, and connection status',
);
});

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

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

describe('Handler Functionality', () => {
it('should handle successful device data retrieval with xctrace fallback', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: `iPhone (12345-ABCDE-FGHIJ-67890) (13.0)
iPad (98765-KLMNO-PQRST-43210) (14.0)
My Device (11111-22222-33333-44444) (15.0)`,
});

const result = await devicesResource.handler(
new URL('xcodebuildmcp://devices'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
expect(result.contents[0].text).toContain('iPhone');
expect(result.contents[0].text).toContain('iPad');
});

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

const result = await devicesResource.handler(
new URL('xcodebuildmcp://devices'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Failed to list devices');
expect(result.contents[0].text).toContain('Command failed');
});

it('should handle spawn errors', async () => {
const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));

const result = await devicesResource.handler(
new URL('xcodebuildmcp://devices'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Error retrieving device data');
expect(result.contents[0].text).toContain('spawn xcrun ENOENT');
});

it('should handle empty device data with xctrace fallback', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});

const result = await devicesResource.handler(
new URL('xcodebuildmcp://devices'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
expect(result.contents[0].text).toContain('Xcode 15 or later');
});

it('should handle device data with next steps guidance', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: `iPhone 15 Pro (12345-ABCDE-FGHIJ-67890) (17.0)`,
});

const result = await devicesResource.handler(
new URL('xcodebuildmcp://devices'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
expect(result.contents[0].text).toContain('iPhone 15 Pro');
});
});
});
111 changes: 111 additions & 0 deletions src/mcp/resources/__tests__/environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, it, expect } from 'vitest';

import environmentResource from '../environment.js';
import { createMockExecutor } from '../../../utils/command.js';

describe('environment resource', () => {
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(environmentResource.uri).toBe('xcodebuildmcp://environment');
});

it('should export correct description', () => {
expect(environmentResource.description).toBe(
'Comprehensive development environment diagnostic information and configuration status',
);
});

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

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

describe('Handler Functionality', () => {
it('should handle successful environment data retrieval', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Mock command output',
});

const result = await environmentResource.handler(
new URL('xcodebuildmcp://environment'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report');
expect(result.contents[0].text).toContain('## System Information');
expect(result.contents[0].text).toContain('## Node.js Information');
expect(result.contents[0].text).toContain('## Dependencies');
expect(result.contents[0].text).toContain('## Environment Variables');
expect(result.contents[0].text).toContain('## Feature Status');
});

it('should handle spawn errors by showing diagnostic info', async () => {
const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));

const result = await environmentResource.handler(
new URL('xcodebuildmcp://environment'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report');
expect(result.contents[0].text).toContain('Error: spawn xcrun ENOENT');
});

it('should include required diagnostic sections', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Mock output',
});

const result = await environmentResource.handler(
new URL('xcodebuildmcp://environment'),
mockExecutor,
);

expect(result.contents[0].text).toContain('## Troubleshooting Tips');
expect(result.contents[0].text).toContain('brew tap cameroncooke/axe');
expect(result.contents[0].text).toContain('INCREMENTAL_BUILDS_ENABLED=1');
expect(result.contents[0].text).toContain('discover_tools');
});

it('should provide feature status information', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'Mock output',
});

const result = await environmentResource.handler(
new URL('xcodebuildmcp://environment'),
mockExecutor,
);

expect(result.contents[0].text).toContain('### UI Automation (axe)');
expect(result.contents[0].text).toContain('### Incremental Builds');
expect(result.contents[0].text).toContain('### Mise Integration');
expect(result.contents[0].text).toContain('## Tool Availability Summary');
});

it('should handle error conditions gracefully', async () => {
const mockExecutor = createMockExecutor({
success: false,
output: '',
error: 'Command failed',
});

const result = await environmentResource.handler(
new URL('xcodebuildmcp://environment'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report');
});
});
});
108 changes: 108 additions & 0 deletions src/mcp/resources/__tests__/swift-packages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, it, expect } from 'vitest';

import swiftPackagesResource from '../swift-packages.js';
import { createMockExecutor } from '../../../utils/command.js';

describe('swift-packages resource', () => {
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(swiftPackagesResource.uri).toBe('xcodebuildmcp://swift-packages');
});

it('should export correct description', () => {
expect(swiftPackagesResource.description).toBe(
'Currently running Swift Package processes with their PIDs and execution status',
);
});

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

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

describe('Handler Functionality', () => {
it('should handle no running processes', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});

const result = await swiftPackagesResource.handler(
new URL('xcodebuildmcp://swift-packages'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('ℹ️ No Swift Package processes currently running.');
expect(result.contents[0].text).toContain('💡 Use swift_package_run to start an executable.');
});

it('should handle spawn errors gracefully', async () => {
const mockExecutor = createMockExecutor(new Error('Process access error'));

const result = await swiftPackagesResource.handler(
new URL('xcodebuildmcp://swift-packages'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
// The swift_package_list logic handles errors gracefully and returns standard "no processes" message
expect(result.contents[0].text).toContain('ℹ️ No Swift Package processes currently running.');
});

it('should provide appropriate response when no processes are running', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});

const result = await swiftPackagesResource.handler(
new URL('xcodebuildmcp://swift-packages'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
const text = result.contents[0].text;
expect(text).toContain('ℹ️ No Swift Package processes currently running.');
expect(text).toContain('💡 Use swift_package_run to start an executable.');
});

it('should handle error responses from swift_package_listLogic', async () => {
const mockExecutor = createMockExecutor({
success: false,
output: '',
error: 'Mock error',
});

const result = await swiftPackagesResource.handler(
new URL('xcodebuildmcp://swift-packages'),
mockExecutor,
);

// Since the logic function doesn't return errors for this simple case,
// it should return the standard "no processes" message
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('ℹ️ No Swift Package processes currently running.');
});

it('should combine multiple content parts correctly', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: '',
});

const result = await swiftPackagesResource.handler(
new URL('xcodebuildmcp://swift-packages'),
mockExecutor,
);

expect(result.contents).toHaveLength(1);
expect(typeof result.contents[0].text).toBe('string');
});
});
});
55 changes: 55 additions & 0 deletions src/mcp/resources/devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Devices Resource Plugin
*
* Provides access to connected Apple devices through MCP resource system.
* This resource reuses the existing list_devices tool logic to maintain consistency.
*/

import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
import { list_devicesLogic } from '../tools/device-shared/list_devices.js';

export default {
uri: 'xcodebuildmcp://devices',
name: 'devices',
description: 'Connected physical Apple devices with their UUIDs, names, and connection status',
mimeType: 'text/plain',
async handler(
uri: URL,
executor: CommandExecutor = getDefaultCommandExecutor(),
): Promise<{ contents: Array<{ text: string }> }> {
try {
log('info', 'Processing devices resource request');

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

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

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

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