Skip to content

Commit 2b273e4

Browse files
committed
feat(backend): enhance Python runtime detection with pyproject.toml support
1 parent 7110aee commit 2b273e4

File tree

2 files changed

+81
-23
lines changed

2 files changed

+81
-23
lines changed

services/backend/src/lib/deployment/runtime-detector.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export interface RuntimeDetectionResult {
2121
dependencies?: Record<string, string>;
2222
devDependencies?: Record<string, string>;
2323
};
24+
pyprojectToml?: { // For Python projects
25+
name?: string;
26+
version?: string;
27+
description?: string;
28+
license?: string;
29+
};
2430
}
2531

2632
/**
@@ -109,6 +115,8 @@ export class RuntimeDetector {
109115
ref: string
110116
): Promise<RuntimeDetectionResult | null> {
111117
// Try requirements.txt first
118+
let mcpSdkInfo: McpSdkInfo | null = null;
119+
112120
try {
113121
const { data: file } = await octokit.repos.getContent({
114122
owner,
@@ -129,15 +137,14 @@ export class RuntimeDetector {
129137
line.trim() === 'mcp'
130138
);
131139

132-
return {
133-
runtime: 'python',
134-
mcp_sdk: {
135-
detected: !!mcpLine,
140+
if (mcpLine) {
141+
mcpSdkInfo = {
142+
detected: true,
136143
version: mcpLine?.split(/==|>=/)[1]?.trim(),
137144
package: 'mcp',
138145
runtime: 'python'
139-
}
140-
};
146+
};
147+
}
141148
}
142149
} catch {
143150
// Continue to pyproject.toml
@@ -155,26 +162,64 @@ export class RuntimeDetector {
155162
if ('content' in file) {
156163
const content = Buffer.from(file.content, 'base64').toString('utf8');
157164

158-
// Simple check for "mcp" in dependencies
159-
const hasMcp = content.includes('"mcp"') || content.includes("'mcp'");
165+
// If we haven't found MCP SDK in requirements.txt, check pyproject.toml
166+
if (!mcpSdkInfo) {
167+
// Check for "mcp" in dependencies (handles both "mcp" and "mcp>=1.0.0" formats)
168+
const hasMcp = /["']mcp["']/.test(content) || /["']mcp[><=]/.test(content);
160169

161-
// Try to extract version from pyproject.toml if present
162-
const versionMatch = content.match(/mcp\s*=\s*["']([^"']+)["']/);
170+
// Try to extract version from pyproject.toml if present
171+
// Matches patterns like "mcp>=1.0.0" or "mcp==1.0.0"
172+
const versionMatch = content.match(/["']mcp[><=]=?\s*([^"',\]]+)["']/);
163173

164-
return {
165-
runtime: 'python',
166-
mcp_sdk: {
167-
detected: hasMcp,
168-
version: versionMatch?.[1],
169-
package: 'mcp',
170-
runtime: 'python'
174+
if (hasMcp) {
175+
mcpSdkInfo = {
176+
detected: true,
177+
version: versionMatch?.[1],
178+
package: 'mcp',
179+
runtime: 'python'
180+
};
171181
}
182+
}
183+
184+
// Extract project metadata from [project] section (always do this)
185+
const nameMatch = content.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
186+
const versionProjMatch = content.match(/^\s*version\s*=\s*["']([^"']+)["']/m);
187+
const descriptionMatch = content.match(/^\s*description\s*=\s*["']([^"']+)["']/m);
188+
189+
// License can be either a simple string or a table with 'text' field
190+
let licenseValue: string | undefined;
191+
const licenseSimpleMatch = content.match(/^\s*license\s*=\s*["']([^"']+)["']/m);
192+
const licenseTableMatch = content.match(/^\s*license\s*=\s*\{\s*text\s*=\s*["']([^"']+)["']/m);
193+
licenseValue = licenseSimpleMatch?.[1] || licenseTableMatch?.[1];
194+
195+
const pyprojectToml = {
196+
name: nameMatch?.[1],
197+
version: versionProjMatch?.[1],
198+
description: descriptionMatch?.[1],
199+
license: licenseValue
172200
};
201+
202+
// Return if we found MCP SDK (from either requirements.txt or pyproject.toml)
203+
if (mcpSdkInfo) {
204+
return {
205+
runtime: 'python',
206+
mcp_sdk: mcpSdkInfo,
207+
pyprojectToml
208+
};
209+
}
173210
}
174211
} catch {
175212
// Continue
176213
}
177214

215+
// If we found MCP SDK in requirements.txt but no pyproject.toml, return without metadata
216+
if (mcpSdkInfo) {
217+
return {
218+
runtime: 'python',
219+
mcp_sdk: mcpSdkInfo
220+
};
221+
}
222+
178223
return null; // No Python files found
179224
}
180225

services/backend/src/services/deploymentValidationService.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ export class DeploymentValidationService {
127127
try {
128128
const { data: repoData } = await octokit.repos.get({ owner, repo });
129129

130-
// Check if repository is empty (no commits)
131-
if (repoData.size === 0 || !repoData.default_branch) {
130+
// Check if repository is empty (only check for default branch, not size)
131+
// Note: GitHub's size field is in KB and can be 0 for very small repos
132+
if (!repoData.default_branch) {
132133
return {
133134
valid: false,
134135
error: `Repository ${owner}/${repo} is empty. Please push code to the repository before deploying.`,
@@ -256,13 +257,25 @@ export class DeploymentValidationService {
256257
// ============================================
257258
// STEP 6: Return Validation Metadata
258259
// ============================================
260+
// For Python projects, use pyprojectToml metadata; for Node.js, use packageJson
261+
const metadata = runtimeResult.runtime === 'python' && runtimeResult.pyprojectToml
262+
? {
263+
name: runtimeResult.pyprojectToml.name,
264+
version: runtimeResult.pyprojectToml.version,
265+
description: runtimeResult.pyprojectToml.description,
266+
license: runtimeResult.pyprojectToml.license
267+
}
268+
: {
269+
name: runtimeResult.packageJson?.name,
270+
version: runtimeResult.packageJson?.version,
271+
description: runtimeResult.packageJson?.description,
272+
license: runtimeResult.packageJson?.license
273+
};
274+
259275
return {
260276
valid: true,
261277
metadata: {
262-
name: runtimeResult.packageJson?.name,
263-
version: runtimeResult.packageJson?.version,
264-
description: runtimeResult.packageJson?.description,
265-
license: runtimeResult.packageJson?.license,
278+
...metadata,
266279
runtime: runtimeResult.runtime,
267280
mcp_sdk: runtimeResult.mcp_sdk,
268281
scripts: runtimeResult.scripts,

0 commit comments

Comments
 (0)