Skip to content

Commit 68a290b

Browse files
committed
feat: migrate from barrel imports to focused facades pattern
This commit implements a comprehensive architectural improvement that replaces the previous barrel import pattern (importing everything from utils/index.ts) with focused facades that expose specific functionality through dedicated index.ts files in subdirectories. Key Changes: - Created 11 focused facade modules in src/utils/ (execution, logging, responses, validation, axe, plugin-registry, xcodemake, template, version, test, log-capture) - Migrated 65+ tool files from barrel imports to focused facade imports - Added ESLint rule to prevent regression to barrel imports - Updated ARCHITECTURE.md to document the new module organization pattern Performance Impact: - Eliminates loading of unused modules, reducing startup time - Improves module resolution speed for Node.js runtime - Prevents circular dependency issues that were risks with large barrel files - Enables better tree-shaking for bundlers Architecture Benefits: - Clear dependency graph - imports explicitly show what functionality is needed - Reduced coupling between unrelated utility components - Better maintainability and code organization - Type-safe imports with focused interfaces
1 parent 8c5ce80 commit 68a290b

78 files changed

Lines changed: 491 additions & 299 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/ARCHITECTURE.md

Lines changed: 158 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,22 @@ XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operat
3838
- MCP server created with stdio transport
3939
- Plugin discovery system initialized
4040

41-
3. **Plugin Discovery & Loading**
42-
- `loadPlugins()` scans `src/mcp/tools/` directory automatically
43-
- `loadResources()` scans `src/mcp/resources/` directory automatically
44-
- Each tool exports standardized interface (`name`, `description`, `schema`, `handler`)
45-
- Tools are self-contained with no external dependencies
46-
- Dynamic vs static mode determines loading behavior
47-
48-
4. **Tool Registration**
49-
- Discovered tools automatically registered with server
41+
3. **Plugin Discovery (Build-Time)**
42+
- A build-time script (`build-plugins/plugin-discovery.ts`) scans the `src/mcp/tools/` and `src/mcp/resources/` directories
43+
- It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps
44+
- This approach improves startup performance by avoiding synchronous file system scans and enables code-splitting
45+
- Tool code is only loaded when needed, reducing initial memory footprint
46+
47+
4. **Plugin & Resource Loading (Runtime)**
48+
- At runtime, `loadPlugins()` and `loadResources()` use the generated loaders from the previous step
49+
- In **Static Mode**, all workflow loaders are executed at startup to register all tools
50+
- In **Dynamic Mode**, only the `discover_tools` tool is registered initially
51+
- The `enableWorkflows` function in `src/core/dynamic-tools.ts` uses generated loaders to dynamically import and register selected workflow tools on demand
52+
53+
5. **Tool Registration**
54+
- Discovered tools automatically registered with server using pre-generated maps
5055
- No manual registration or configuration required
51-
- Environment variables can still control dynamic tool discovery
56+
- Environment variables control dynamic tool discovery behavior
5257

5358
5. **Request Handling**
5459
- MCP client calls tool → server routes to tool handler
@@ -85,6 +90,66 @@ Tools are self-contained units that export a standardized interface. They don't
8590
- Zod schemas for runtime validation
8691
- Generic type constraints ensure compile-time safety
8792

93+
## Module Organization and Import Strategy
94+
95+
### Focused Facades Pattern
96+
97+
XcodeBuildMCP has migrated from a traditional "barrel file" export pattern (`src/utils/index.ts`) to a more structured **focused facades** pattern. Each distinct area of functionality within `src/utils` is exposed through its own `index.ts` file in a dedicated subdirectory.
98+
99+
**Example Structure:**
100+
101+
```
102+
src/utils/
103+
├── execution/
104+
│ └── index.ts # Facade for CommandExecutor, FileSystemExecutor
105+
├── logging/
106+
│ └── index.ts # Facade for the logger
107+
├── responses/
108+
│ └── index.ts # Facade for error types and response creators
109+
├── validation/
110+
│ └── index.ts # Facade for validation utilities
111+
├── axe/
112+
│ └── index.ts # Facade for axe UI automation helpers
113+
├── plugin-registry/
114+
│ └── index.ts # Facade for plugin system utilities
115+
├── xcodemake/
116+
│ └── index.ts # Facade for xcodemake utilities
117+
├── template/
118+
│ └── index.ts # Facade for template management utilities
119+
├── version/
120+
│ └── index.ts # Facade for version information
121+
├── test/
122+
│ └── index.ts # Facade for test utilities
123+
├── log-capture/
124+
│ └── index.ts # Facade for log capture utilities
125+
└── index.ts # Deprecated barrel file (legacy/external use only)
126+
```
127+
128+
This approach offers several architectural benefits:
129+
130+
- **Clear Dependencies**: It makes the dependency graph explicit. Importing from `utils/execution` clearly indicates a dependency on command execution logic
131+
- **Reduced Coupling**: Modules only import the functionality they need, reducing coupling between unrelated utility components
132+
- **Prevention of Circular Dependencies**: It's much harder to create circular dependencies, which were a risk with the large barrel file
133+
- **Improved Tree-Shaking**: Bundlers can more effectively eliminate unused code
134+
- **Performance**: Eliminates loading of unused modules, reducing startup time and memory usage
135+
136+
### ESLint Enforcement
137+
138+
To maintain this architecture, an ESLint rule in `eslint.config.js` explicitly forbids importing from the deprecated barrel file within the `src/` directory.
139+
140+
**ESLint Rule Snippet** (`eslint.config.js`):
141+
142+
```javascript
143+
'no-restricted-imports': ['error', {
144+
patterns: [{
145+
group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js'],
146+
message: 'Barrel imports from utils/index.js are prohibited. Use focused facade imports instead (e.g., utils/logging/index.js, utils/execution/index.js).'
147+
}]
148+
}],
149+
```
150+
151+
This rule prevents regression to the previous barrel import pattern and ensures all new code follows the focused facade architecture.
152+
88153
## Component Details
89154

90155
### Entry Points
@@ -116,12 +181,12 @@ MCP server wrapper providing:
116181
### Tool Discovery System
117182

118183
#### `src/core/plugin-registry.ts`
119-
Automatic plugin loading system:
120-
- Scans `src/mcp/tools/` directory structure using glob patterns
121-
- Dynamically imports plugin modules
122-
- Validates plugin interface compliance
123-
- Handles both default exports and named exports (for re-exports)
124-
- Supports workflow group metadata via `index.js` files
184+
Runtime plugin loading system that leverages build-time generated code:
185+
- Uses `WORKFLOW_LOADERS` and `WORKFLOW_METADATA` maps from the generated `src/core/generated-plugins.ts` file
186+
- `loadWorkflowGroups()` iterates through the loaders, dynamically importing each workflow module using `await loader()`
187+
- Validates that each imported module contains the required `workflow` metadata export
188+
- Aggregates all tools from the loaded workflows into a single map
189+
- This system eliminates runtime file system scanning, providing significant startup performance boost
125190

126191
#### `src/core/plugin-types.ts`
127192
Plugin type definitions:
@@ -131,49 +196,74 @@ Plugin type definitions:
131196

132197
### Tool Implementation
133198

134-
Each plugin (`src/mcp/tools/*/*.js`) follows this standardized pattern:
199+
Each tool is implemented in TypeScript and follows a standardized pattern that separates the core business logic from the MCP handler boilerplate. This is achieved using the `createTypedTool` factory, which provides compile-time and runtime type safety.
135200

136-
```javascript
137-
// 1. Import dependencies and schemas
201+
**Standard Tool Pattern** (`src/mcp/tools/some-workflow/some_tool.ts`):
202+
203+
```typescript
138204
import { z } from 'zod';
139-
import { log } from '../../src/utils/logger.js';
140-
import { executeCommand } from '../../src/utils/command.js';
205+
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
206+
import type { CommandExecutor } from '../../../utils/execution/index.js';
207+
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
208+
import { log } from '../../../utils/logging/index.js';
209+
import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.js';
210+
211+
// 1. Define the Zod schema for parameters
212+
const someToolSchema = z.object({
213+
requiredParam: z.string().describe('Description for AI'),
214+
optionalParam: z.boolean().optional().describe('Optional parameter'),
215+
});
141216

142-
// 2. Define and export plugin
143-
export default {
144-
name: 'tool_name',
145-
description: 'Tool description for AI agents',
146-
147-
// 3. Define parameter schema
148-
schema: {
149-
requiredParam: z.string().describe('Description for AI'),
150-
optionalParam: z.string().optional().describe('Optional parameter')
151-
},
217+
// 2. Infer the parameter type from the schema
218+
type SomeToolParams = z.infer<typeof someToolSchema>;
219+
220+
// 3. Implement the core logic in a separate, testable function
221+
// This function receives strongly-typed parameters and an injected executor.
222+
export async function someToolLogic(
223+
params: SomeToolParams,
224+
executor: CommandExecutor,
225+
): Promise<ToolResponse> {
226+
log('info', `Executing some_tool with param: ${params.requiredParam}`);
152227

153-
// 4. Implement handler function
154-
async handler(params) {
155-
try {
156-
// 5. Execute tool logic using shared utilities
157-
const result = await executeCommand(['some', 'command']);
158-
159-
// 6. Return standardized response
160-
return {
161-
content: [{ type: 'text', text: result.output }],
162-
isError: false
163-
};
164-
} catch (error) {
165-
return {
166-
content: [{ type: 'text', text: `Error: ${error.message}` }],
167-
isError: true
168-
};
228+
try {
229+
const result = await executor(['some', 'command'], 'Some Tool Operation');
230+
231+
if (!result.success) {
232+
return createErrorResponse('Operation failed', result.error);
169233
}
234+
235+
return createTextResponse(`✅ Success: ${result.output}`);
236+
} catch (error) {
237+
const errorMessage = error instanceof Error ? error.message : String(error);
238+
return createErrorResponse('Tool execution failed', errorMessage);
170239
}
240+
}
241+
242+
// 4. Export the tool definition for auto-discovery
243+
export default {
244+
name: 'some_tool',
245+
description: 'Tool description for AI agents. Example: some_tool({ requiredParam: "value" })',
246+
schema: someToolSchema.shape, // Expose shape for MCP SDK
247+
248+
// 5. Create the handler using the type-safe factory
249+
handler: createTypedTool(
250+
someToolSchema,
251+
someToolLogic,
252+
getDefaultCommandExecutor,
253+
),
171254
};
172255
```
173256

257+
This pattern ensures that:
258+
- The `someToolLogic` function is highly testable via dependency injection
259+
- Zod handles all runtime parameter validation automatically
260+
- The handler is type-safe, preventing unsafe access to parameters
261+
- Import paths use focused facades for clear dependency management
262+
```
263+
174264
### MCP Resources System
175265
176-
XcodeBuildMCP provides dual interfaces: traditional MCP tools and efficient MCP resources for supported clients. Resources are located in `src/mcp/resources/` and are automatically discovered. For more details on creating resources, see the [Plugin Development Guide](docs/PLUGIN_DEVELOPMENT.md).
266+
XcodeBuildMCP provides dual interfaces: traditional MCP tools and efficient MCP resources for supported clients. Resources are located in `src/mcp/resources/` and are automatically discovered **at build time**. The build process generates `src/core/generated-resources.ts`, which contains dynamic loaders for each resource, improving startup performance. For more details on creating resources, see the [Plugin Development Guide](docs/PLUGIN_DEVELOPMENT.md).
177267
178268
#### Resource Architecture
179269
@@ -201,7 +291,8 @@ Resources can reuse existing tool logic for consistency:
201291

202292
```typescript
203293
// src/mcp/resources/some_resource.ts
204-
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
294+
import { log } from '../../utils/logging/index.js';
295+
import { getDefaultCommandExecutor, CommandExecutor } from '../../utils/execution/index.js';
205296
import { getSomeResourceLogic } from '../tools/some-workflow/get_some_resource.js';
206297

207298
// Testable resource logic separated from MCP handler
@@ -330,12 +421,12 @@ For detailed guidelines, see the [Testing Guide](docs/TESTING.md).
330421

331422
### Test Structure Example
332423

333-
Tests inject mock "executors" for external interactions like command-line execution or file system access. This allows for deterministic testing of tool logic without mocking the implementation itself.
424+
Tests inject mock "executors" for external interactions like command-line execution or file system access. This allows for deterministic testing of tool logic without mocking the implementation itself. The project provides helper functions like `createMockExecutor` and `createMockFileSystemExecutor` in `src/test-utils/mock-executors.ts` to facilitate this pattern.
334425

335426
```typescript
336427
import { describe, it, expect } from 'vitest';
337-
import { toolNameLogic } from '../tool-file.js'; // Import the logic function
338-
import { createMockExecutor } from '../../../utils/test-common.js';
428+
import { someToolLogic } from '../tool-file.js'; // Import the logic function
429+
import { createMockExecutor } from '../../../test-utils/mock-executors.js';
339430

340431
describe('Tool Name', () => {
341432
it('should execute successfully', async () => {
@@ -346,7 +437,7 @@ describe('Tool Name', () => {
346437
});
347438

348439
// 2. Call the tool's logic function, injecting the mock executor
349-
const result = await toolNameLogic({ param: 'value' }, mockExecutor);
440+
const result = await someToolLogic({ requiredParam: 'value' }, mockExecutor);
350441

351442
// 3. Assert the final result
352443
expect(result).toEqual({
@@ -367,15 +458,24 @@ describe('Tool Name', () => {
367458
```
368459
- Reads version from `package.json`
369460
- Generates `src/version.ts`
461+
462+
2. **Plugin & Resource Loader Generation**
463+
- The `build-plugins/plugin-discovery.ts` script is executed
464+
- It scans `src/mcp/tools/` and `src/mcp/resources/` to find all workflows and resources
465+
- It generates `src/core/generated-plugins.ts` and `src/core/generated-resources.ts` with dynamic import maps
466+
- This eliminates runtime file system scanning and enables code-splitting
467+
468+
3. **TypeScript Compilation**
469+
- `tsup` compiles the TypeScript source, including the newly generated files, into JavaScript
370470
- Compiles TypeScript with tsup
371471

372-
2. **Build Configuration** (`tsup.config.ts`)
472+
4. **Build Configuration** (`tsup.config.ts`)
373473
- Entry points: `index.ts`, `doctor-cli.ts`
374474
- Output format: ESM
375475
- Target: Node 18+
376476
- Source maps enabled
377477

378-
3. **Distribution Structure**
478+
5. **Distribution Structure**
379479
```
380480
build/
381481
├── index.js # Main server executable
@@ -417,6 +517,9 @@ The guide covers:
417517

418518
### Startup Performance
419519

520+
- **Build-Time Plugin Discovery**: The server avoids expensive and slow file system scans at startup by using pre-generated loader maps. This is the single most significant performance optimization
521+
- **Code-Splitting**: In Dynamic Mode, tool code is only loaded into memory when its workflow is enabled, reducing the initial memory footprint and parse time
522+
- **Focused Facades**: Using targeted imports instead of a large barrel file improves module resolution speed for the Node.js runtime
420523
- **Lazy Loading**: Tools only initialized when registered
421524
- **Selective Registration**: Fewer tools = faster startup
422525
- **Minimal Dependencies**: Fast module resolution

eslint.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export default [
5656
'@typescript-eslint/prefer-as-const': 'warn',
5757
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
5858
'@typescript-eslint/prefer-optional-chain': 'warn',
59+
60+
// Prevent barrel imports to maintain architectural improvements
61+
'no-restricted-imports': ['error', {
62+
patterns: [{
63+
group: ['**/utils/index.js', '../utils/index.js', '../../utils/index.js', '../../../utils/index.js'],
64+
message: 'Barrel imports from utils/index.js are prohibited. Use focused facade imports instead (e.g., utils/logging/index.js, utils/execution/index.js).'
65+
}]
66+
}],
5967
},
6068
},
6169
{

src/core/resources.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313

1414
import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js';
1515
import { ReadResourceResult } from '@camsoft/mcp-sdk/types.js';
16-
import { log, CommandExecutor } from '../utils/index.js';
16+
import { log } from '../utils/logging/index.js';
17+
import type { CommandExecutor } from '../utils/execution/index.js';
1718
import { RESOURCE_LOADERS } from './generated-resources.js';
1819

1920
/**

src/doctor-cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import { version } from './version.js';
1111
import { doctorLogic } from './mcp/tools/doctor/doctor.js';
12-
import { getDefaultCommandExecutor } from './utils/index.js';
12+
import { getDefaultCommandExecutor } from './utils/execution/index.js';
1313

1414
async function runDoctor(): Promise<void> {
1515
try {

src/mcp/resources/devices.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* This resource reuses the existing list_devices tool logic to maintain consistency.
66
*/
77

8-
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
8+
import { log } from '../../utils/logging/index.js';
9+
import type { CommandExecutor } from '../../utils/execution/index.js';
10+
import { getDefaultCommandExecutor } from '../../utils/execution/index.js';
911
import { list_devicesLogic } from '../tools/device/list_devices.js';
1012

1113
// Testable resource logic separated from MCP handler

src/mcp/resources/simulators.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
* This resource reuses the existing list_sims tool logic to maintain consistency.
66
*/
77

8-
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
8+
import { log } from '../../utils/logging/index.js';
9+
import { getDefaultCommandExecutor } from '../../utils/execution/index.js';
10+
import type { CommandExecutor } from '../../utils/execution/index.js';
911
import { list_simsLogic } from '../tools/simulator/list_sims.js';
1012

1113
// Testable resource logic separated from MCP handler

src/mcp/tools/device/build_device.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
import { z } from 'zod';
99
import { ToolResponse, XcodePlatform } from '../../../types/common.js';
10-
import { executeXcodeBuildCommand } from '../../../utils/index.js';
11-
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
10+
import { executeXcodeBuildCommand } from '../../../utils/build-utils.js';
11+
import type { CommandExecutor } from '../../../utils/execution/index.js';
12+
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
1213
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
1314
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.js';
1415

src/mcp/tools/device/get_device_app_path.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
import { z } from 'zod';
99
import { ToolResponse, XcodePlatform } from '../../../types/common.js';
10-
import { log } from '../../../utils/index.js';
11-
import { createTextResponse } from '../../../utils/index.js';
12-
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/index.js';
10+
import { log } from '../../../utils/logging/index.js';
11+
import { createTextResponse } from '../../../utils/responses/index.js';
12+
import type { CommandExecutor } from '../../../utils/execution/index.js';
13+
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
1314
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
1415
import { nullifyEmptyStrings } from '../../../utils/schema-helpers.js';
1516

src/mcp/tools/device/install_app_device.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
import { z } from 'zod';
99
import { ToolResponse } from '../../../types/common.js';
10-
import { log, CommandExecutor, getDefaultCommandExecutor } from '../../../utils/index.js';
10+
import { log } from '../../../utils/logging/index.js';
11+
import type { CommandExecutor } from '../../../utils/execution/index.js';
12+
import { getDefaultCommandExecutor } from '../../../utils/execution/index.js';
1113
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
1214

1315
// Define schema as ZodObject

0 commit comments

Comments
 (0)