Skip to content

Commit 110e4b3

Browse files
authored
Merge pull request #24 from oslabs-beta/paython-mcp
2 parents 15d5e4e + 175fcdb commit 110e4b3

File tree

4 files changed

+198
-26
lines changed

4 files changed

+198
-26
lines changed
979 KB
Binary file not shown.

server/agent/wizardAgent.js

Lines changed: 167 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ dotenv.config();
66
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
77

88
// Helper: call MCP routes dynamically, with error handling
9-
async function callMCPTool(tool, input) {
9+
async function callMCPTool(tool, input, cookie) {
1010
try {
1111
const response = await fetch(`http://localhost:3000/mcp/v1/${tool}`, {
1212
method: "POST",
1313
headers: {
1414
"Content-Type": "application/json",
15-
"Authorization": `Bearer ${process.env.MCP_SESSION_TOKEN}`,
15+
"Cookie": cookie || (process.env.MCP_SESSION_TOKEN ? `mcp_session=${process.env.MCP_SESSION_TOKEN}` : ""),
1616
},
1717
body: JSON.stringify(input),
1818
});
@@ -25,6 +25,13 @@ async function callMCPTool(tool, input) {
2525

2626
// Wizard Agent Core
2727
export async function runWizardAgent(userPrompt) {
28+
// Normalize userPrompt into a consistent text form + extract cookie
29+
const userPromptText =
30+
typeof userPrompt === "string"
31+
? userPrompt
32+
: userPrompt?.prompt || "";
33+
34+
const cookie = userPrompt?.cookie || "";
2835
const systemPrompt = `
2936
You are the MCP Wizard Agent.
3037
You have full access to the following connected tools and APIs:
@@ -43,55 +50,68 @@ export async function runWizardAgent(userPrompt) {
4350
- “Show recent commits for [username/repo]” → use \`github_adapter\` with \`{ action: "commits", repo: "[username/repo]" }\`
4451
- “List workflows for [username/repo]” → use \`github_adapter\` with \`{ action: "workflows", repo: "[username/repo]" }\`
4552
- “List repos”, “List repositories”, or “repositories” → use \`repo_reader\` with optional \`{ username: "...", user_id: "..." }\`
53+
Valid CI/CD template types are ONLY:
54+
- node_app
55+
- python_app
56+
- container_service
57+
58+
When selecting or generating a pipeline template, you MUST return one of these exact values.
59+
Never invent new template names. If unsure, default to "node_app".
4660
`;
4761

4862
const completion = await client.chat.completions.create({
4963
model: "gpt-4o-mini",
5064
messages: [
5165
{ role: "system", content: systemPrompt },
52-
{ role: "user", content: userPrompt },
66+
{ role: "user", content: typeof userPrompt === "string" ? userPrompt : userPrompt.prompt },
5367
],
5468
});
5569

5670
const decision = completion.choices[0].message.content;
5771
console.log("\n🤖 Agent decided:", decision);
5872

73+
let agentMeta = {
74+
agent_decision: decision,
75+
tool_called: null,
76+
};
77+
5978
// Tool mapping using regex patterns
6079
const toolMap = {
6180
repo_reader: /\b(list repos|list repositories|repositories|repo_reader)\b/i,
6281
pipeline_generator: /\bpipeline\b/i,
82+
pipeline_commit: /\b(yes commit|commit (the )?(pipeline|workflow|file)|apply (the )?(pipeline|workflow)|save (the )?(pipeline|workflow)|push (the )?(pipeline|workflow))\b/i,
6383
oidc_adapter: /\b(role|jenkins)\b/i,
6484
github_adapter: /\b(github|repo info|repository|[\w-]+\/[\w-]+)\b/i,
6585
};
6686

6787
for (const [toolName, pattern] of Object.entries(toolMap)) {
68-
if (pattern.test(decision)) {
88+
if (pattern.test(decision) || pattern.test(userPromptText)) {
6989
console.log('🔧 Triggering MCP tool:', toolName);
7090

7191
// --- Extract context dynamically from userPrompt or decision ---
7292
// Prefer explicit labels like: "repo owner/name", "template node_app", "provider aws"
73-
const labeledRepo = userPrompt.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i)
93+
const labeledRepo = userPromptText.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i)
7494
|| decision.match(/\brepo\s+([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/i);
75-
const genericRepo = (userPrompt + " " + decision).match(/\b(?!ci\/cd\b)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/);
95+
const genericRepo = (userPromptText + " " + decision).match(/\b(?!ci\/cd\b)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\b/);
7696
const repo = (labeledRepo?.[1] || genericRepo?.[1] || null);
7797

78-
const labeledProvider = userPrompt.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i)
98+
const labeledProvider = userPromptText.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i)
7999
|| decision.match(/\bprovider\s+(aws|jenkins|gcp|azure)\b/i);
80-
const genericProvider = userPrompt.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i)
100+
const genericProvider = userPromptText.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i)
81101
|| decision.match(/\b(aws|jenkins|github actions|gcp|azure)\b/i);
82102
const provider = (labeledProvider?.[1] || genericProvider?.[1] || null)?.toLowerCase().replace(/\s+/g, ' ');
83103

84-
const labeledTemplate = userPrompt.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i)
104+
const labeledTemplate = userPromptText.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i)
85105
|| decision.match(/\btemplate\s+([a-z_][a-z0-9_]+)\b/i);
86-
const genericTemplate = userPrompt.match(/\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i)
106+
const genericTemplate = userPromptText.match(/\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i)
87107
|| decision.match(/\b(node_app|python_app|container_service|node|python|react|express|django|flask|java|go)\b/i);
88108
const template = (labeledTemplate?.[1] || genericTemplate?.[1] || null)?.toLowerCase();
89109

90110
if (toolName === "repo_reader") {
91111
// Extract optional username, user_id, and repo info
92-
const usernameMatch = userPrompt.match(/\busername[:=]?\s*([\w-]+)\b/i);
93-
const userIdMatch = userPrompt.match(/\buser[_ ]?id[:=]?\s*([\w-]+)\b/i);
94-
const repoMatch = userPrompt.match(/\b([\w-]+\/[\w-]+)\b/);
112+
const usernameMatch = userPromptText.match(/\busername[:=]?\s*([\w-]+)\b/i);
113+
const userIdMatch = userPromptText.match(/\buser[_ ]?id[:=]?\s*([\w-]+)\b/i);
114+
const repoMatch = userPromptText.match(/\b([\w-]+\/[\w-]+)\b/);
95115

96116
const payload = {};
97117
if (usernameMatch) payload.username = usernameMatch[1];
@@ -102,7 +122,14 @@ export async function runWizardAgent(userPrompt) {
102122
payload.repo = `${username}/${repo}`;
103123
}
104124

105-
return await callMCPTool("repo_reader", payload);
125+
agentMeta.tool_called = "repo_reader";
126+
const output = await callMCPTool("repo_reader", payload, cookie);
127+
return {
128+
success: true,
129+
agent_decision: agentMeta.agent_decision,
130+
tool_called: agentMeta.tool_called,
131+
tool_output: output
132+
};
106133
}
107134

108135
if (toolName === "pipeline_generator") {
@@ -121,7 +148,7 @@ export async function runWizardAgent(userPrompt) {
121148
// Fetch GitHub repo details before pipeline generation
122149
let repoInfo = null;
123150
try {
124-
const info = await callMCPTool("github_adapter", { action: "info", repo });
151+
const info = await callMCPTool("github_adapter", { action: "info", repo }, cookie);
125152
if (info?.data?.success) {
126153
repoInfo = info.data;
127154
console.log(`📦 Retrieved repo info from GitHub:`, repoInfo);
@@ -151,6 +178,13 @@ export async function runWizardAgent(userPrompt) {
151178
if (payload.template === "python") payload.template = "python_app";
152179
if (payload.template === "container") payload.template = "container_service";
153180

181+
// --- Validate template against allowed values ---
182+
const allowedTemplates = ["node_app", "python_app", "container_service"];
183+
if (!allowedTemplates.includes(payload.template)) {
184+
console.warn("⚠ Invalid template inferred:", payload.template, "— auto-correcting to node_app.");
185+
payload.template = "node_app";
186+
}
187+
154188
// --- Preserve repo context globally ---
155189
if (!payload.repo && globalThis.LAST_REPO_USED) {
156190
payload.repo = globalThis.LAST_REPO_USED;
@@ -166,17 +200,126 @@ export async function runWizardAgent(userPrompt) {
166200
}
167201

168202
console.log("🧩 Final payload to pipeline_generator:", payload);
169-
return await callMCPTool("pipeline_generator", payload);
203+
agentMeta.tool_called = "pipeline_generator";
204+
const output = await callMCPTool("pipeline_generator", payload, cookie);
205+
206+
// Extract YAML for confirmation step
207+
const generatedYaml =
208+
output?.data?.data?.generated_yaml ||
209+
output?.tool_output?.data?.generated_yaml ||
210+
null;
211+
212+
// Store YAML globally for future commit step
213+
globalThis.LAST_GENERATED_YAML = generatedYaml;
214+
215+
// Return confirmation-required structure
216+
return {
217+
success: true,
218+
requires_confirmation: true,
219+
message: "A pipeline has been generated. Would you like me to commit this workflow file to your repository?",
220+
agent_decision: agentMeta.agent_decision,
221+
tool_called: agentMeta.tool_called,
222+
generated_yaml: generatedYaml,
223+
pipeline_metadata: output
224+
};
225+
}
226+
227+
if (toolName === "pipeline_commit") {
228+
console.log("📝 Commit intent detected.");
229+
230+
// ❗ Guard: Prevent confusing "repo commit history" with "pipeline commit"
231+
if (/recent commits|commit history|see commits|show commits|view commits/i.test(decision + " " + userPromptText)) {
232+
console.log("⚠ Not pipeline commit. Detected intention to view repo commit history.");
233+
agentMeta.tool_called = "github_adapter";
234+
235+
const repoForCommits = repo || globalThis.LAST_REPO_USED;
236+
if (!repoForCommits) {
237+
return {
238+
success: false,
239+
error: "Please specify a repository, e.g. 'show commits for user/repo'."
240+
};
241+
}
242+
243+
const output = await callMCPTool("github_adapter", { action: "commits", repo: repoForCommits }, cookie);
244+
245+
return {
246+
success: true,
247+
agent_decision: agentMeta.agent_decision,
248+
tool_called: agentMeta.tool_called,
249+
tool_output: output
250+
};
251+
}
252+
253+
// Ensure we have a repo
254+
const commitRepo = repo || globalThis.LAST_REPO_USED;
255+
if (!commitRepo) {
256+
return {
257+
success: false,
258+
error: "I don’t know which repository to commit to. Please specify the repo (e.g., 'commit to user/repo')."
259+
};
260+
}
261+
262+
// Extract YAML from userPrompt or fallback to last generated YAML
263+
const yamlMatch = userPromptText.match(/```yaml([\s\S]*?)```/i);
264+
const yamlFromPrompt = yamlMatch ? yamlMatch[1].trim() : null;
265+
266+
const yaml =
267+
yamlFromPrompt ||
268+
globalThis.LAST_GENERATED_YAML ||
269+
null;
270+
271+
if (!yaml) {
272+
return {
273+
success: false,
274+
error: "I don’t have a pipeline YAML to commit. Please generate one first."
275+
};
276+
}
277+
278+
// Save YAML globally for future edits
279+
globalThis.LAST_GENERATED_YAML = yaml;
280+
281+
const commitPayload = {
282+
repoFullName: commitRepo,
283+
yaml,
284+
branch: "main",
285+
path: ".github/workflows/ci.yml"
286+
};
287+
288+
agentMeta.tool_called = "pipeline_commit";
289+
const output = await callMCPTool("pipeline_commit", commitPayload, cookie);
290+
291+
return {
292+
success: true,
293+
agent_decision: agentMeta.agent_decision,
294+
tool_called: agentMeta.tool_called,
295+
committed_repo: commitRepo,
296+
committed_path: ".github/workflows/ci.yml",
297+
tool_output: output
298+
};
170299
}
171300

172301
if (toolName === "oidc_adapter") {
173302
const payload = provider ? { provider } : {};
174-
return await callMCPTool("oidc_adapter", payload);
303+
agentMeta.tool_called = "oidc_adapter";
304+
const output = await callMCPTool("oidc_adapter", payload, cookie);
305+
return {
306+
success: true,
307+
agent_decision: agentMeta.agent_decision,
308+
tool_called: agentMeta.tool_called,
309+
tool_output: output
310+
};
175311
}
176312

177313
if (toolName === "github_adapter") {
178314
if (repo) {
179-
return await callMCPTool("github/info", { repo });
315+
agentMeta.tool_called = "github_adapter";
316+
const output = await callMCPTool("github/info", { repo }, cookie);
317+
return {
318+
success: true,
319+
agent_decision: agentMeta.agent_decision,
320+
tool_called: agentMeta.tool_called,
321+
tool_output: output
322+
};
180323
} else {
181324
console.warn("⚠️ Missing repo for GitHub info retrieval.");
182325
return {
@@ -188,7 +331,12 @@ export async function runWizardAgent(userPrompt) {
188331
}
189332
}
190333

191-
return { message: "No matching tool found. Try asking about a repo, pipeline, or AWS role." };
334+
return {
335+
success: false,
336+
agent_decision: agentMeta.agent_decision,
337+
tool_called: null,
338+
message: "No matching tool found. Try asking about a repo, pipeline, or AWS role."
339+
};
192340
}
193341

194342
// Example local test (can comment out for production)

server/agent/wizardAgent_prompts.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ node server/agent/wizardAgent.js "Show the languages used in PVeazie951/soloProj
2727

2828
---
2929

30-
## 📂 Repo Contents / Files
30+
## TODO 📂 Repo Contents / Files
3131

3232
These interact with the `list_root` and `contents` actions in the GitHub adapter.
3333

@@ -49,7 +49,7 @@ These prompts trigger the pipeline generator logic and repo inference system.
4949
```bash
5050
node server/agent/wizardAgent.js "Generate a pipeline for my Node app"
5151
node server/agent/wizardAgent.js "Generate a CI/CD pipeline for PVeazie951/soloProject"
52-
node server/agent/wizardAgent.js "What kind of pipeline should I use for PVeazie951/google-extention-ai-summarizer"
52+
node server/agent/wizardAgent.js "What kind of CICD pipeline should I use for PVeazie951/google-extention-ai-summarizer"
5353
node server/agent/wizardAgent.js "Create a GitHub Actions pipeline for my soloProject repo"
5454
node server/agent/wizardAgent.js "What kind of deployment pipeline fits this repo"
5555
node server/agent/wizardAgent.js "Use Jenkins instead of GitHub Actions for deployment"

server/routes/agent.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { runWizardAgent } from '../agent/wizardAgent.js';
77
import { pipeline_generator } from '../tools/pipeline_generator.js';
88
import { repo_reader } from '../tools/repo_reader.js';
99
import { oidc_adapter } from '../tools/oidc_adapter.js';
10+
import { requireSession } from '../lib/requireSession.js';
1011

1112
const router = express.Router();
1213

1314
// Trigger full pipeline wizard (MVP agent)
14-
router.post('/wizard', async (req, res) => {
15+
router.post('/wizard', requireSession, async (req, res) => {
1516
try {
1617
const { repoUrl, provider, branch } = req.body;
1718
if (!repoUrl || !provider || !branch) {
@@ -22,16 +23,39 @@ router.post('/wizard', async (req, res) => {
2223
error: 'Missing required fields: repoUrl, provider, branch',
2324
});
2425
}
25-
const result = await runWizardAgent({ repoUrl, provider, branch });
26+
const result = await runWizardAgent({
27+
repoUrl,
28+
provider,
29+
branch,
30+
cookie: req.headers.cookie
31+
});
2632
res.json({ success: true, data: result });
2733
} catch (err) {
2834
console.error('Wizard Error:', err);
2935
res.status(500).json({ success: false, error: err.message });
3036
}
3137
});
3238

39+
// Trigger wizard agent with AI prompt
40+
router.post('/wizard/ai', requireSession, async (req, res) => {
41+
try {
42+
const { prompt } = req.body;
43+
if (!prompt) {
44+
return res.status(400).json({ success: false, error: 'Missing required field: prompt' });
45+
}
46+
const result = await runWizardAgent({
47+
prompt,
48+
cookie: req.headers.cookie
49+
});
50+
res.json({ success: true, data: result });
51+
} catch (err) {
52+
console.error('Wizard AI Error:', err);
53+
res.status(500).json({ success: false, error: err.message });
54+
}
55+
});
56+
3357
// Generate pipeline only
34-
router.post('/pipeline', async (req, res) => {
58+
router.post('/pipeline', requireSession, async (req, res) => {
3559
try {
3660
const { repoUrl } = req.body;
3761
if (!repoUrl) {
@@ -51,7 +75,7 @@ router.post('/pipeline', async (req, res) => {
5175
});
5276

5377
// Read repository metadata
54-
router.post('/analyze', async (req, res) => {
78+
router.post('/analyze', requireSession, async (req, res) => {
5579
try {
5680
const { repoUrl } = req.body;
5781
if (!repoUrl) {
@@ -67,7 +91,7 @@ router.post('/analyze', async (req, res) => {
6791
});
6892

6993
// Deploy to AWS (via OIDC)
70-
router.post('/deploy', async (req, res) => {
94+
router.post('/deploy', requireSession, async (req, res) => {
7195
try {
7296
const { provider } = req.body;
7397
if (!provider) {

0 commit comments

Comments
 (0)