Skip to content

Commit 2ceb272

Browse files
feat: add MCP resources for list_devices, diagnostic, and swift_package_list
Add three new MCP resources to complement existing tools: - mcp://xcodebuild/devices - Connected physical Apple devices - mcp://xcodebuild/environment - Development environment diagnostics - mcp://xcodebuild/swift-packages - Running Swift Package processes Key features: - Reuse existing tool logic for consistency - Comprehensive test coverage following no-vitest-mocking guidelines - Proper error handling and logging - Maintain backward compatibility with existing tools - Auto-discovered by build system (4 total resources now available) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Cameron Cooke <cameroncooke@users.noreply.github.com>
1 parent 881c5d2 commit 2ceb272

6 files changed

Lines changed: 496 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import devicesResource from '../devices.js';
4+
import { createMockExecutor } from '../../../utils/command.js';
5+
6+
describe('devices resource', () => {
7+
describe('Export Field Validation', () => {
8+
it('should export correct uri', () => {
9+
expect(devicesResource.uri).toBe('xcodebuildmcp://devices');
10+
});
11+
12+
it('should export correct description', () => {
13+
expect(devicesResource.description).toBe(
14+
'Connected physical Apple devices with their UUIDs, names, and connection status',
15+
);
16+
});
17+
18+
it('should export correct mimeType', () => {
19+
expect(devicesResource.mimeType).toBe('text/plain');
20+
});
21+
22+
it('should export handler function', () => {
23+
expect(typeof devicesResource.handler).toBe('function');
24+
});
25+
});
26+
27+
describe('Handler Functionality', () => {
28+
it('should handle successful device data retrieval with xctrace fallback', async () => {
29+
const mockExecutor = createMockExecutor({
30+
success: true,
31+
output: `iPhone (12345-ABCDE-FGHIJ-67890) (13.0)
32+
iPad (98765-KLMNO-PQRST-43210) (14.0)
33+
My Device (11111-22222-33333-44444) (15.0)`,
34+
});
35+
36+
const result = await devicesResource.handler(
37+
new URL('xcodebuildmcp://devices'),
38+
mockExecutor,
39+
);
40+
41+
expect(result.contents).toHaveLength(1);
42+
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
43+
expect(result.contents[0].text).toContain('iPhone');
44+
expect(result.contents[0].text).toContain('iPad');
45+
});
46+
47+
it('should handle command execution failure', async () => {
48+
const mockExecutor = createMockExecutor({
49+
success: false,
50+
output: '',
51+
error: 'Command failed',
52+
});
53+
54+
const result = await devicesResource.handler(
55+
new URL('xcodebuildmcp://devices'),
56+
mockExecutor,
57+
);
58+
59+
expect(result.contents).toHaveLength(1);
60+
expect(result.contents[0].text).toContain('Failed to list devices');
61+
expect(result.contents[0].text).toContain('Command failed');
62+
});
63+
64+
it('should handle spawn errors', async () => {
65+
const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));
66+
67+
const result = await devicesResource.handler(
68+
new URL('xcodebuildmcp://devices'),
69+
mockExecutor,
70+
);
71+
72+
expect(result.contents).toHaveLength(1);
73+
expect(result.contents[0].text).toContain('Error retrieving device data');
74+
expect(result.contents[0].text).toContain('spawn xcrun ENOENT');
75+
});
76+
77+
it('should handle empty device data with xctrace fallback', async () => {
78+
const mockExecutor = createMockExecutor({
79+
success: true,
80+
output: '',
81+
});
82+
83+
const result = await devicesResource.handler(
84+
new URL('xcodebuildmcp://devices'),
85+
mockExecutor,
86+
);
87+
88+
expect(result.contents).toHaveLength(1);
89+
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
90+
expect(result.contents[0].text).toContain('Xcode 15 or later');
91+
});
92+
93+
it('should handle device data with next steps guidance', async () => {
94+
const mockExecutor = createMockExecutor({
95+
success: true,
96+
output: `iPhone 15 Pro (12345-ABCDE-FGHIJ-67890) (17.0)`,
97+
});
98+
99+
const result = await devicesResource.handler(
100+
new URL('xcodebuildmcp://devices'),
101+
mockExecutor,
102+
);
103+
104+
expect(result.contents).toHaveLength(1);
105+
expect(result.contents[0].text).toContain('Device listing (xctrace output)');
106+
expect(result.contents[0].text).toContain('iPhone 15 Pro');
107+
});
108+
});
109+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import environmentResource from '../environment.js';
4+
import { createMockExecutor } from '../../../utils/command.js';
5+
6+
describe('environment resource', () => {
7+
describe('Export Field Validation', () => {
8+
it('should export correct uri', () => {
9+
expect(environmentResource.uri).toBe('xcodebuildmcp://environment');
10+
});
11+
12+
it('should export correct description', () => {
13+
expect(environmentResource.description).toBe(
14+
'Comprehensive development environment diagnostic information and configuration status',
15+
);
16+
});
17+
18+
it('should export correct mimeType', () => {
19+
expect(environmentResource.mimeType).toBe('text/plain');
20+
});
21+
22+
it('should export handler function', () => {
23+
expect(typeof environmentResource.handler).toBe('function');
24+
});
25+
});
26+
27+
describe('Handler Functionality', () => {
28+
it('should handle successful environment data retrieval', async () => {
29+
const mockExecutor = createMockExecutor({
30+
success: true,
31+
output: 'Mock command output',
32+
});
33+
34+
const result = await environmentResource.handler(
35+
new URL('xcodebuildmcp://environment'),
36+
mockExecutor,
37+
);
38+
39+
expect(result.contents).toHaveLength(1);
40+
expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report');
41+
expect(result.contents[0].text).toContain('## System Information');
42+
expect(result.contents[0].text).toContain('## Node.js Information');
43+
expect(result.contents[0].text).toContain('## Dependencies');
44+
expect(result.contents[0].text).toContain('## Environment Variables');
45+
expect(result.contents[0].text).toContain('## Feature Status');
46+
});
47+
48+
it('should handle spawn errors by showing diagnostic info', async () => {
49+
const mockExecutor = createMockExecutor(new Error('spawn xcrun ENOENT'));
50+
51+
const result = await environmentResource.handler(
52+
new URL('xcodebuildmcp://environment'),
53+
mockExecutor,
54+
);
55+
56+
expect(result.contents).toHaveLength(1);
57+
expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report');
58+
expect(result.contents[0].text).toContain('Error: spawn xcrun ENOENT');
59+
});
60+
61+
it('should include required diagnostic sections', async () => {
62+
const mockExecutor = createMockExecutor({
63+
success: true,
64+
output: 'Mock output',
65+
});
66+
67+
const result = await environmentResource.handler(
68+
new URL('xcodebuildmcp://environment'),
69+
mockExecutor,
70+
);
71+
72+
expect(result.contents[0].text).toContain('## Troubleshooting Tips');
73+
expect(result.contents[0].text).toContain('brew tap cameroncooke/axe');
74+
expect(result.contents[0].text).toContain('INCREMENTAL_BUILDS_ENABLED=1');
75+
expect(result.contents[0].text).toContain('discover_tools');
76+
});
77+
78+
it('should provide feature status information', async () => {
79+
const mockExecutor = createMockExecutor({
80+
success: true,
81+
output: 'Mock output',
82+
});
83+
84+
const result = await environmentResource.handler(
85+
new URL('xcodebuildmcp://environment'),
86+
mockExecutor,
87+
);
88+
89+
expect(result.contents[0].text).toContain('### UI Automation (axe)');
90+
expect(result.contents[0].text).toContain('### Incremental Builds');
91+
expect(result.contents[0].text).toContain('### Mise Integration');
92+
expect(result.contents[0].text).toContain('## Tool Availability Summary');
93+
});
94+
95+
it('should handle error conditions gracefully', async () => {
96+
const mockExecutor = createMockExecutor({
97+
success: false,
98+
output: '',
99+
error: 'Command failed',
100+
});
101+
102+
const result = await environmentResource.handler(
103+
new URL('xcodebuildmcp://environment'),
104+
mockExecutor,
105+
);
106+
107+
expect(result.contents).toHaveLength(1);
108+
expect(result.contents[0].text).toContain('# XcodeBuildMCP Diagnostic Report');
109+
});
110+
});
111+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import swiftPackagesResource from '../swift-packages.js';
4+
import { createMockExecutor } from '../../../utils/command.js';
5+
6+
describe('swift-packages resource', () => {
7+
describe('Export Field Validation', () => {
8+
it('should export correct uri', () => {
9+
expect(swiftPackagesResource.uri).toBe('xcodebuildmcp://swift-packages');
10+
});
11+
12+
it('should export correct description', () => {
13+
expect(swiftPackagesResource.description).toBe(
14+
'Currently running Swift Package processes with their PIDs and execution status',
15+
);
16+
});
17+
18+
it('should export correct mimeType', () => {
19+
expect(swiftPackagesResource.mimeType).toBe('text/plain');
20+
});
21+
22+
it('should export handler function', () => {
23+
expect(swiftPackagesResource.handler).toBeDefined();
24+
expect(typeof swiftPackagesResource.handler).toBe('function');
25+
});
26+
});
27+
28+
describe('Handler Functionality', () => {
29+
it('should handle no running processes', async () => {
30+
const mockExecutor = createMockExecutor({
31+
success: true,
32+
output: '',
33+
});
34+
35+
const result = await swiftPackagesResource.handler(
36+
new URL('xcodebuildmcp://swift-packages'),
37+
mockExecutor,
38+
);
39+
40+
expect(result.contents).toHaveLength(1);
41+
expect(result.contents[0].text).toContain('ℹ️ No Swift Package processes currently running.');
42+
expect(result.contents[0].text).toContain('💡 Use swift_package_run to start an executable.');
43+
});
44+
45+
it('should handle spawn errors gracefully', async () => {
46+
const mockExecutor = createMockExecutor(new Error('Process access error'));
47+
48+
const result = await swiftPackagesResource.handler(
49+
new URL('xcodebuildmcp://swift-packages'),
50+
mockExecutor,
51+
);
52+
53+
expect(result.contents).toHaveLength(1);
54+
// The swift_package_list logic handles errors gracefully and returns standard "no processes" message
55+
expect(result.contents[0].text).toContain('ℹ️ No Swift Package processes currently running.');
56+
});
57+
58+
it('should provide appropriate response when no processes are running', async () => {
59+
const mockExecutor = createMockExecutor({
60+
success: true,
61+
output: '',
62+
});
63+
64+
const result = await swiftPackagesResource.handler(
65+
new URL('xcodebuildmcp://swift-packages'),
66+
mockExecutor,
67+
);
68+
69+
expect(result.contents).toHaveLength(1);
70+
const text = result.contents[0].text;
71+
expect(text).toContain('ℹ️ No Swift Package processes currently running.');
72+
expect(text).toContain('💡 Use swift_package_run to start an executable.');
73+
});
74+
75+
it('should handle error responses from swift_package_listLogic', async () => {
76+
const mockExecutor = createMockExecutor({
77+
success: false,
78+
output: '',
79+
error: 'Mock error',
80+
});
81+
82+
const result = await swiftPackagesResource.handler(
83+
new URL('xcodebuildmcp://swift-packages'),
84+
mockExecutor,
85+
);
86+
87+
// Since the logic function doesn't return errors for this simple case,
88+
// it should return the standard "no processes" message
89+
expect(result.contents).toHaveLength(1);
90+
expect(result.contents[0].text).toContain('ℹ️ No Swift Package processes currently running.');
91+
});
92+
93+
it('should combine multiple content parts correctly', async () => {
94+
const mockExecutor = createMockExecutor({
95+
success: true,
96+
output: '',
97+
});
98+
99+
const result = await swiftPackagesResource.handler(
100+
new URL('xcodebuildmcp://swift-packages'),
101+
mockExecutor,
102+
);
103+
104+
expect(result.contents).toHaveLength(1);
105+
expect(typeof result.contents[0].text).toBe('string');
106+
});
107+
});
108+
});

src/mcp/resources/devices.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Devices Resource Plugin
3+
*
4+
* Provides access to connected Apple devices through MCP resource system.
5+
* This resource reuses the existing list_devices tool logic to maintain consistency.
6+
*/
7+
8+
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
9+
import { list_devicesLogic } from '../tools/device-shared/list_devices.js';
10+
11+
export default {
12+
uri: 'xcodebuildmcp://devices',
13+
name: 'devices',
14+
description: 'Connected physical Apple devices with their UUIDs, names, and connection status',
15+
mimeType: 'text/plain',
16+
async handler(
17+
uri: URL,
18+
executor: CommandExecutor = getDefaultCommandExecutor(),
19+
): Promise<{ contents: Array<{ text: string }> }> {
20+
try {
21+
log('info', 'Processing devices resource request');
22+
23+
const result = await list_devicesLogic({}, executor);
24+
25+
if (result.isError) {
26+
const errorText = result.content[0]?.text;
27+
throw new Error(
28+
typeof errorText === 'string' ? errorText : 'Failed to retrieve device data',
29+
);
30+
}
31+
32+
return {
33+
contents: [
34+
{
35+
text:
36+
typeof result.content[0]?.text === 'string'
37+
? result.content[0].text
38+
: 'No device data available',
39+
},
40+
],
41+
};
42+
} catch (error) {
43+
const errorMessage = error instanceof Error ? error.message : String(error);
44+
log('error', `Error in devices resource handler: ${errorMessage}`);
45+
46+
return {
47+
contents: [
48+
{
49+
text: `Error retrieving device data: ${errorMessage}`,
50+
},
51+
],
52+
};
53+
}
54+
},
55+
};

0 commit comments

Comments
 (0)