Skip to content

Commit 451eee4

Browse files
authored
Smithery (getsentry#159)
* refactor: migrate to Zod v4 * Add support for Smithery
1 parent c502e79 commit 451eee4

File tree

142 files changed

+5422
-1040
lines changed

Some content is hidden

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

142 files changed

+5422
-1040
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ jobs:
2626
- name: Install dependencies
2727
run: npm ci
2828

29-
- name: Build
29+
- name: Build (tsup)
30+
run: npm run build:tsup
31+
32+
- name: Build (Smithery)
3033
run: npm run build
3134

3235
- name: Lint

.github/workflows/release.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ jobs:
4848
- name: Bundle AXe artifacts
4949
run: npm run bundle:axe
5050

51-
- name: Build TypeScript
51+
- name: Build TypeScript (tsup)
52+
run: npm run build:tsup
53+
54+
- name: Build Smithery bundle
5255
run: npm run build
5356

5457
- name: Run tests

.smithery/index.cjs

Lines changed: 786 additions & 0 deletions
Large diffs are not rendered by default.

Dockerfile

Lines changed: 0 additions & 38 deletions
This file was deleted.

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,21 @@ claude mcp add XcodeBuildMCP npx xcodebuildmcp@latest -e INCREMENTAL_BUILDS_ENAB
170170

171171
##### Smithery
172172

173-
To install XcodeBuildMCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@cameroncooke/XcodeBuildMCP):
173+
XcodeBuildMCP runs as a local (stdio) server when installed via [Smithery](https://smithery.ai/server/@cameroncooke/XcodeBuildMCP). You can configure it in the Smithery session UI or via environment variables.
174174

175175
```bash
176+
# Claude Desktop / Claude Code
176177
npx -y @smithery/cli install @cameroncooke/XcodeBuildMCP --client claude
178+
179+
# Cursor
180+
npx -y @smithery/cli install @cameroncooke/XcodeBuildMCP --client cursor
181+
182+
# VS Code
183+
npx -y @smithery/cli install @cameroncooke/XcodeBuildMCP --client vscode
177184
```
178185

186+
If your client isn't listed, run `npx smithery install --help` to see supported values for `--client`.
187+
179188
> [!IMPORTANT]
180189
> Please note that XcodeBuildMCP will request xcodebuild to skip macro validation. This is to avoid errors when building projects that use Swift Macros.
181190

build-plugins/plugin-discovery.ts

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ export function createPluginDiscoveryPlugin(): Plugin {
1616
build.onStart(async () => {
1717
try {
1818
await generateWorkflowLoaders();
19+
await generateResourceLoaders();
1920
} catch (error) {
20-
console.error('Failed to generate workflow loaders:', error);
21+
console.error('Failed to generate loaders:', error);
2122
throw error;
2223
}
2324
});
2425
},
2526
};
2627
}
2728

28-
async function generateWorkflowLoaders(): Promise<void> {
29-
const pluginsDir = path.resolve(process.cwd(), 'src/plugins');
29+
export async function generateWorkflowLoaders(): Promise<void> {
30+
const pluginsDir = path.resolve(process.cwd(), 'src/mcp/tools');
3031

3132
if (!existsSync(pluginsDir)) {
3233
throw new Error(`Plugins directory not found: ${pluginsDir}`);
@@ -41,7 +42,8 @@ async function generateWorkflowLoaders(): Promise<void> {
4142
const workflowMetadata: Record<string, WorkflowMetadata> = {};
4243

4344
for (const dirName of workflowDirs) {
44-
const indexPath = join(pluginsDir, dirName, 'index.ts');
45+
const dirPath = join(pluginsDir, dirName);
46+
const indexPath = join(dirPath, 'index.ts');
4547

4648
// Check if workflow has index.ts file
4749
if (!existsSync(indexPath)) {
@@ -55,11 +57,26 @@ async function generateWorkflowLoaders(): Promise<void> {
5557
const metadata = extractWorkflowMetadata(indexContent);
5658

5759
if (metadata) {
58-
// Generate dynamic import for this workflow
59-
workflowLoaders[dirName] = `() => import('../plugins/${dirName}/index.js')`;
60+
// Find all tool files in this workflow directory
61+
const toolFiles = readdirSync(dirPath, { withFileTypes: true })
62+
.filter((dirent) => dirent.isFile())
63+
.map((dirent) => dirent.name)
64+
.filter(
65+
(name) =>
66+
(name.endsWith('.ts') || name.endsWith('.js')) &&
67+
name !== 'index.ts' &&
68+
name !== 'index.js' &&
69+
!name.endsWith('.test.ts') &&
70+
!name.endsWith('.test.js') &&
71+
name !== 'active-processes.ts',
72+
);
73+
74+
workflowLoaders[dirName] = generateWorkflowLoader(dirName, toolFiles);
6075
workflowMetadata[dirName] = metadata;
6176

62-
console.log(`✅ Discovered workflow: ${dirName} - ${metadata.name}`);
77+
console.log(
78+
`✅ Discovered workflow: ${dirName} - ${metadata.name} (${toolFiles.length} tools)`,
79+
);
6380
} else {
6481
console.warn(`⚠️ Skipping ${dirName}: invalid workflow metadata`);
6582
}
@@ -80,6 +97,31 @@ async function generateWorkflowLoaders(): Promise<void> {
8097
console.log(`🔧 Generated workflow loaders for ${Object.keys(workflowLoaders).length} workflows`);
8198
}
8299

100+
function generateWorkflowLoader(workflowName: string, toolFiles: string[]): string {
101+
const toolImports = toolFiles
102+
.map((file, index) => {
103+
const toolName = file.replace(/\.(ts|js)$/, '');
104+
return `const tool_${index} = await import('../mcp/tools/${workflowName}/${toolName}.js').then(m => m.default)`;
105+
})
106+
.join(';\n ');
107+
108+
const toolExports = toolFiles
109+
.map((file, index) => {
110+
const toolName = file.replace(/\.(ts|js)$/, '');
111+
return `'${toolName}': tool_${index}`;
112+
})
113+
.join(',\n ');
114+
115+
return `async () => {
116+
const { workflow } = await import('../mcp/tools/${workflowName}/index.js');
117+
${toolImports ? toolImports + ';\n ' : ''}
118+
return {
119+
workflow,
120+
${toolExports ? toolExports : ''}
121+
};
122+
}`;
123+
}
124+
83125
function extractWorkflowMetadata(content: string): WorkflowMetadata | null {
84126
try {
85127
// Simple regex to extract workflow export object
@@ -114,7 +156,13 @@ function generatePluginsFileContent(
114156
workflowMetadata: Record<string, WorkflowMetadata>,
115157
): string {
116158
const loaderEntries = Object.entries(workflowLoaders)
117-
.map(([key, loader]) => ` '${key}': ${loader}`)
159+
.map(([key, loader]) => {
160+
const indentedLoader = loader
161+
.split('\n')
162+
.map((line, index) => (index === 0 ? ` '${key}': ${line}` : ` ${line}`))
163+
.join('\n');
164+
return indentedLoader;
165+
})
118166
.join(',\n');
119167

120168
const metadataEntries = Object.entries(workflowMetadata)
@@ -143,3 +191,59 @@ ${metadataEntries}
143191
};
144192
`;
145193
}
194+
195+
export async function generateResourceLoaders(): Promise<void> {
196+
const resourcesDir = path.resolve(process.cwd(), 'src/mcp/resources');
197+
198+
if (!existsSync(resourcesDir)) {
199+
console.log('Resources directory not found, skipping resource generation');
200+
return;
201+
}
202+
203+
const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
204+
.filter((dirent) => dirent.isFile())
205+
.map((dirent) => dirent.name)
206+
.filter(
207+
(name) =>
208+
(name.endsWith('.ts') || name.endsWith('.js')) &&
209+
!name.endsWith('.test.ts') &&
210+
!name.endsWith('.test.js') &&
211+
!name.startsWith('__'),
212+
);
213+
214+
const resourceLoaders: Record<string, string> = {};
215+
216+
for (const fileName of resourceFiles) {
217+
const resourceName = fileName.replace(/\.(ts|js)$/, '');
218+
resourceLoaders[resourceName] = `async () => {
219+
const module = await import('../mcp/resources/${resourceName}.js');
220+
return module.default;
221+
}`;
222+
223+
console.log(`✅ Discovered resource: ${resourceName}`);
224+
}
225+
226+
const generatedContent = generateResourcesFileContent(resourceLoaders);
227+
const outputPath = path.resolve(process.cwd(), 'src/core/generated-resources.ts');
228+
229+
const fs = await import('fs');
230+
await fs.promises.writeFile(outputPath, generatedContent, 'utf8');
231+
232+
console.log(`🔧 Generated resource loaders for ${Object.keys(resourceLoaders).length} resources`);
233+
}
234+
235+
function generateResourcesFileContent(resourceLoaders: Record<string, string>): string {
236+
const loaderEntries = Object.entries(resourceLoaders)
237+
.map(([key, loader]) => ` '${key}': ${loader}`)
238+
.join(',\n');
239+
240+
return `// AUTO-GENERATED - DO NOT EDIT
241+
// This file is generated by the plugin discovery esbuild plugin
242+
243+
export const RESOURCE_LOADERS = {
244+
${loaderEntries}
245+
};
246+
247+
export type ResourceName = keyof typeof RESOURCE_LOADERS;
248+
`;
249+
}

0 commit comments

Comments
 (0)