@girardmedia/bootspring 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/agents/README.md +93 -0
- package/agents/api-expert/context.md +416 -0
- package/agents/architecture-expert/context.md +454 -0
- package/agents/backend-expert/context.md +483 -0
- package/agents/code-review-expert/context.md +365 -0
- package/agents/database-expert/context.md +250 -0
- package/agents/devops-expert/context.md +446 -0
- package/agents/frontend-expert/context.md +364 -0
- package/agents/index.js +140 -0
- package/agents/performance-expert/context.md +377 -0
- package/agents/security-expert/context.md +343 -0
- package/agents/testing-expert/context.md +414 -0
- package/agents/ui-ux-expert/context.md +448 -0
- package/agents/vercel-expert/context.md +426 -0
- package/bin/bootspring.js +310 -0
- package/cli/agent.js +337 -0
- package/cli/context.js +194 -0
- package/cli/dashboard.js +150 -0
- package/cli/generate.js +294 -0
- package/cli/init.js +410 -0
- package/cli/loop.js +421 -0
- package/cli/mcp.js +241 -0
- package/cli/memory.js +303 -0
- package/cli/orchestrator.js +400 -0
- package/cli/plugin.js +451 -0
- package/cli/quality.js +332 -0
- package/cli/skill.js +369 -0
- package/cli/task.js +628 -0
- package/cli/telemetry.js +114 -0
- package/cli/todo.js +614 -0
- package/cli/update.js +312 -0
- package/core/config.js +245 -0
- package/core/context.js +329 -0
- package/core/entitlements.js +209 -0
- package/core/index.js +43 -0
- package/core/policies.js +68 -0
- package/core/telemetry.js +247 -0
- package/core/utils.js +380 -0
- package/dashboard/server.js +818 -0
- package/docs/integrations/claude-code.md +42 -0
- package/docs/integrations/codex.md +42 -0
- package/docs/mcp-api-platform.md +102 -0
- package/generators/generate.js +598 -0
- package/generators/index.js +18 -0
- package/hooks/context-detector.js +177 -0
- package/hooks/index.js +35 -0
- package/hooks/prompt-enhancer.js +289 -0
- package/intelligence/git-memory.js +551 -0
- package/intelligence/index.js +59 -0
- package/intelligence/orchestrator.js +964 -0
- package/intelligence/prd.js +447 -0
- package/intelligence/recommendation-weights.json +18 -0
- package/intelligence/recommendations.js +234 -0
- package/mcp/capabilities.js +71 -0
- package/mcp/contracts/mcp-contract.v1.json +497 -0
- package/mcp/registry.js +213 -0
- package/mcp/response-formatter.js +462 -0
- package/mcp/server.js +99 -0
- package/mcp/tools/agent-tool.js +137 -0
- package/mcp/tools/capabilities-tool.js +54 -0
- package/mcp/tools/context-tool.js +49 -0
- package/mcp/tools/dashboard-tool.js +58 -0
- package/mcp/tools/generate-tool.js +46 -0
- package/mcp/tools/loop-tool.js +134 -0
- package/mcp/tools/memory-tool.js +180 -0
- package/mcp/tools/orchestrator-tool.js +232 -0
- package/mcp/tools/plugin-tool.js +76 -0
- package/mcp/tools/quality-tool.js +47 -0
- package/mcp/tools/skill-tool.js +233 -0
- package/mcp/tools/telemetry-tool.js +95 -0
- package/mcp/tools/todo-tool.js +133 -0
- package/package.json +98 -0
- package/plugins/index.js +141 -0
- package/quality/index.js +380 -0
- package/quality/lint-budgets.json +19 -0
- package/skills/index.js +787 -0
- package/skills/patterns/README.md +163 -0
- package/skills/patterns/api/route-handler.md +217 -0
- package/skills/patterns/api/server-action.md +249 -0
- package/skills/patterns/auth/clerk.md +132 -0
- package/skills/patterns/database/prisma.md +180 -0
- package/skills/patterns/payments/stripe.md +272 -0
- package/skills/patterns/security/validation.md +268 -0
- package/skills/patterns/testing/vitest.md +307 -0
- package/templates/bootspring.config.js +83 -0
- package/templates/mcp.json +9 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP memory tool module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function getToolDefinition() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'bootspring_memory',
|
|
8
|
+
description: 'Access git-based project memory. Extract learnings, search history, get file-specific context.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
action: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['summary', 'search', 'files', 'stats'],
|
|
15
|
+
description: 'Action: summary (categorized learnings), search (by keyword), files (for specific files), stats (repo overview)'
|
|
16
|
+
},
|
|
17
|
+
query: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Search query (for search action)'
|
|
20
|
+
},
|
|
21
|
+
files: {
|
|
22
|
+
type: 'array',
|
|
23
|
+
items: { type: 'string' },
|
|
24
|
+
description: 'File paths to get learnings for (for files action)'
|
|
25
|
+
},
|
|
26
|
+
limit: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'Max learnings to return (default: 20)'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
required: ['action']
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createHandler({ gitMemory }) {
|
|
37
|
+
return async (args) => {
|
|
38
|
+
const { action, query, files, limit = 20 } = args;
|
|
39
|
+
|
|
40
|
+
if (!gitMemory.isGitRepo()) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: JSON.stringify({
|
|
45
|
+
error: 'Not a git repository',
|
|
46
|
+
hint: 'Initialize git with: git init'
|
|
47
|
+
}, null, 2)
|
|
48
|
+
}]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
switch (action) {
|
|
53
|
+
case 'summary': {
|
|
54
|
+
const result = gitMemory.extractLearnings({ limit });
|
|
55
|
+
if (result.error) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: `Error: ${result.error}` }]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const grouped = gitMemory.groupByCategory(result.learnings);
|
|
61
|
+
const summary = {};
|
|
62
|
+
|
|
63
|
+
for (const [category, items] of Object.entries(grouped)) {
|
|
64
|
+
const config = gitMemory.MEMORY_CATEGORIES[category];
|
|
65
|
+
summary[category] = {
|
|
66
|
+
icon: config?.icon || '📝',
|
|
67
|
+
label: config?.label || category,
|
|
68
|
+
count: items.length,
|
|
69
|
+
top: items.slice(0, 3).map(i => ({
|
|
70
|
+
hash: i.hash,
|
|
71
|
+
summary: i.summary
|
|
72
|
+
}))
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: JSON.stringify({
|
|
80
|
+
totalLearnings: result.learnings.length,
|
|
81
|
+
categories: summary
|
|
82
|
+
}, null, 2)
|
|
83
|
+
}]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'search': {
|
|
88
|
+
if (!query) {
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: JSON.stringify({ error: 'query required for search' }, null, 2)
|
|
93
|
+
}]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const result = gitMemory.extractLearnings({ limit: 100 });
|
|
97
|
+
const matches = gitMemory.searchLearnings(result.learnings, query);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{
|
|
101
|
+
type: 'text',
|
|
102
|
+
text: JSON.stringify({
|
|
103
|
+
query,
|
|
104
|
+
matches: matches.slice(0, limit).map(m => ({
|
|
105
|
+
hash: m.hash,
|
|
106
|
+
summary: m.summary,
|
|
107
|
+
categories: m.categories,
|
|
108
|
+
details: m.details?.slice(0, 200)
|
|
109
|
+
}))
|
|
110
|
+
}, null, 2)
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'files': {
|
|
116
|
+
if (!files || files.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: JSON.stringify({ error: 'files array required' }, null, 2)
|
|
121
|
+
}]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const learnings = gitMemory.getLearningsForFiles(files, { limit });
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
content: [{
|
|
128
|
+
type: 'text',
|
|
129
|
+
text: JSON.stringify({
|
|
130
|
+
files,
|
|
131
|
+
learnings: learnings.map(l => ({
|
|
132
|
+
hash: l.hash,
|
|
133
|
+
file: l.file,
|
|
134
|
+
summary: l.summary,
|
|
135
|
+
categories: l.categories
|
|
136
|
+
}))
|
|
137
|
+
}, null, 2)
|
|
138
|
+
}]
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'stats': {
|
|
143
|
+
const stats = gitMemory.getRepoStats();
|
|
144
|
+
const result = gitMemory.extractLearnings({ limit: 100 });
|
|
145
|
+
const grouped = gitMemory.groupByCategory(result.learnings);
|
|
146
|
+
|
|
147
|
+
const categoryCounts = {};
|
|
148
|
+
for (const [cat, items] of Object.entries(grouped)) {
|
|
149
|
+
categoryCounts[cat] = items.length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
content: [{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: JSON.stringify({
|
|
156
|
+
...stats,
|
|
157
|
+
categoryCounts
|
|
158
|
+
}, null, 2)
|
|
159
|
+
}]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
default:
|
|
164
|
+
return {
|
|
165
|
+
content: [{
|
|
166
|
+
type: 'text',
|
|
167
|
+
text: JSON.stringify({
|
|
168
|
+
error: `Unknown action: ${action}`,
|
|
169
|
+
validActions: ['summary', 'search', 'files', 'stats']
|
|
170
|
+
}, null, 2)
|
|
171
|
+
}]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
getToolDefinition,
|
|
179
|
+
createHandler
|
|
180
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP orchestrator tool module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const MAX_RECOMMENDATION_LIMIT = 50;
|
|
6
|
+
const DEFAULT_RECOMMENDATION_LIMIT = 5;
|
|
7
|
+
|
|
8
|
+
function getToolDefinition() {
|
|
9
|
+
return {
|
|
10
|
+
name: 'bootspring_orchestrator',
|
|
11
|
+
description: 'Intelligent agent coordination. Analyze context, suggest agents, manage workflows.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
action: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: ['analyze', 'suggest', 'recommend', 'workflows', 'workflow', 'start', 'next', 'checkpoint', 'status'],
|
|
18
|
+
description: 'Action: analyze context, suggest agents, recommend workflows/skills, list workflows, show workflow, start/advance workflow, or status'
|
|
19
|
+
},
|
|
20
|
+
context: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'Context text to analyze (for analyze/suggest actions)'
|
|
23
|
+
},
|
|
24
|
+
workflow: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Workflow name (for workflow action)'
|
|
27
|
+
},
|
|
28
|
+
signal: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Completion signal ref for action=checkpoint (exact text, partial text, or 1-based index)'
|
|
31
|
+
},
|
|
32
|
+
limit: {
|
|
33
|
+
type: 'number',
|
|
34
|
+
description: 'Max recommendations to return for action=recommend (default: 5)'
|
|
35
|
+
},
|
|
36
|
+
mode: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
enum: ['local', 'server'],
|
|
39
|
+
description: 'Optional entitlement mode override'
|
|
40
|
+
},
|
|
41
|
+
entitled: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
description: 'Optional entitlement override for premium workflows'
|
|
44
|
+
},
|
|
45
|
+
tier: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: 'Optional tier override: free, pro, team, enterprise'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ['action']
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createHandler({ intelligence, entitlements, recommendations, trackTelemetry, format }) {
|
|
56
|
+
return async (args) => {
|
|
57
|
+
const {
|
|
58
|
+
action,
|
|
59
|
+
context: contextText,
|
|
60
|
+
workflow: workflowName,
|
|
61
|
+
signal,
|
|
62
|
+
mode,
|
|
63
|
+
entitled,
|
|
64
|
+
tier
|
|
65
|
+
} = args;
|
|
66
|
+
const accessOptions = { mode, entitled, tier };
|
|
67
|
+
|
|
68
|
+
switch (action) {
|
|
69
|
+
case 'analyze':
|
|
70
|
+
case 'suggest': {
|
|
71
|
+
const analysis = intelligence.analyzeContext(contextText || '');
|
|
72
|
+
return {
|
|
73
|
+
content: [{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: JSON.stringify({
|
|
76
|
+
phase: analysis.phase,
|
|
77
|
+
phaseName: analysis.phaseConfig?.name,
|
|
78
|
+
suggestions: analysis.suggestions,
|
|
79
|
+
skills: analysis.skills
|
|
80
|
+
}, null, 2)
|
|
81
|
+
}]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
case 'recommend': {
|
|
85
|
+
const requestedLimit = Number(args.limit);
|
|
86
|
+
const validLimit = Number.isFinite(requestedLimit) && requestedLimit > 0
|
|
87
|
+
? Math.min(requestedLimit, MAX_RECOMMENDATION_LIMIT)
|
|
88
|
+
: DEFAULT_RECOMMENDATION_LIMIT;
|
|
89
|
+
const result = recommendations.recommend({
|
|
90
|
+
contextText: contextText || '',
|
|
91
|
+
limit: validLimit,
|
|
92
|
+
accessOptions
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: JSON.stringify(result, null, 2)
|
|
98
|
+
}]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
case 'workflows': {
|
|
102
|
+
const workflows = intelligence.listWorkflows();
|
|
103
|
+
const filtered = entitlements.filterAccessibleWorkflows(workflows, accessOptions);
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: JSON.stringify({
|
|
108
|
+
workflows: filtered.allowed,
|
|
109
|
+
hiddenCount: filtered.denied.length
|
|
110
|
+
}, null, 2)
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case 'workflow': {
|
|
115
|
+
if (!workflowName) throw new Error('Workflow name required');
|
|
116
|
+
const workflow = intelligence.getWorkflow(workflowName);
|
|
117
|
+
if (!workflow) throw new Error(`Unknown workflow: ${workflowName}`);
|
|
118
|
+
const decision = entitlements.checkWorkflowAccess(workflow, accessOptions);
|
|
119
|
+
if (!decision.allowed) {
|
|
120
|
+
trackTelemetry('premium_prompted', {
|
|
121
|
+
capability: 'workflow_pack',
|
|
122
|
+
workflow: workflowName,
|
|
123
|
+
tier: workflow.tier || 'free',
|
|
124
|
+
mode: decision.context?.mode,
|
|
125
|
+
userTier: decision.context?.tier,
|
|
126
|
+
reason: decision.code
|
|
127
|
+
});
|
|
128
|
+
throw new Error(decision.reason);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify(workflow, null, 2)
|
|
134
|
+
}]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case 'start': {
|
|
138
|
+
if (!workflowName) throw new Error('Workflow name required');
|
|
139
|
+
const workflow = intelligence.getWorkflow(workflowName);
|
|
140
|
+
if (!workflow) throw new Error(`Unknown workflow: ${workflowName}`);
|
|
141
|
+
const decision = entitlements.checkWorkflowAccess(workflow, accessOptions);
|
|
142
|
+
if (!decision.allowed) {
|
|
143
|
+
trackTelemetry('premium_prompted', {
|
|
144
|
+
capability: 'workflow_pack',
|
|
145
|
+
workflow: workflowName,
|
|
146
|
+
tier: workflow.tier || 'free',
|
|
147
|
+
mode: decision.context?.mode,
|
|
148
|
+
userTier: decision.context?.tier,
|
|
149
|
+
reason: decision.code
|
|
150
|
+
});
|
|
151
|
+
throw new Error(decision.reason);
|
|
152
|
+
}
|
|
153
|
+
const result = intelligence.startWorkflow(workflowName);
|
|
154
|
+
if (!result.success) throw new Error(result.error || 'Failed to start workflow');
|
|
155
|
+
return {
|
|
156
|
+
content: [{
|
|
157
|
+
type: 'text',
|
|
158
|
+
text: JSON.stringify({
|
|
159
|
+
...result,
|
|
160
|
+
signalProgress: intelligence.getWorkflowSignalProgress(workflowName)
|
|
161
|
+
}, null, 2)
|
|
162
|
+
}]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
case 'next': {
|
|
166
|
+
const result = intelligence.advanceWorkflow();
|
|
167
|
+
if (!result.success) throw new Error(result.error || 'Failed to advance workflow');
|
|
168
|
+
const activeName = intelligence.loadState().activeWorkflow;
|
|
169
|
+
return {
|
|
170
|
+
content: [{
|
|
171
|
+
type: 'text',
|
|
172
|
+
text: JSON.stringify({
|
|
173
|
+
...result,
|
|
174
|
+
signalProgress: activeName ? intelligence.getWorkflowSignalProgress(activeName) : null
|
|
175
|
+
}, null, 2)
|
|
176
|
+
}]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
case 'checkpoint': {
|
|
180
|
+
const targetWorkflowName = workflowName || intelligence.loadState().activeWorkflow;
|
|
181
|
+
if (targetWorkflowName) {
|
|
182
|
+
const workflow = intelligence.getWorkflow(targetWorkflowName);
|
|
183
|
+
if (!workflow) throw new Error(`Unknown workflow: ${targetWorkflowName}`);
|
|
184
|
+
const decision = entitlements.checkWorkflowAccess(workflow, accessOptions);
|
|
185
|
+
if (!decision.allowed) {
|
|
186
|
+
trackTelemetry('premium_prompted', {
|
|
187
|
+
capability: 'workflow_pack',
|
|
188
|
+
workflow: targetWorkflowName,
|
|
189
|
+
tier: workflow.tier || 'free',
|
|
190
|
+
mode: decision.context?.mode,
|
|
191
|
+
userTier: decision.context?.tier,
|
|
192
|
+
reason: decision.code
|
|
193
|
+
});
|
|
194
|
+
throw new Error(decision.reason);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const result = intelligence.markWorkflowCheckpoint(workflowName, signal);
|
|
198
|
+
if (!result.success) throw new Error(result.error || 'Failed to mark workflow checkpoint');
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: JSON.stringify(result, null, 2)
|
|
203
|
+
}]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
case 'status': {
|
|
207
|
+
const state = intelligence.loadState();
|
|
208
|
+
const phase = intelligence.getCurrentPhase();
|
|
209
|
+
const agents = intelligence.getAvailableAgents();
|
|
210
|
+
return format.orchestratorStatus({
|
|
211
|
+
currentPhase: phase,
|
|
212
|
+
lastAnalysis: state.lastAnalysis,
|
|
213
|
+
activeWorkflow: state.activeWorkflow,
|
|
214
|
+
availableAgents: agents.length,
|
|
215
|
+
recentSuggestions: state.suggestions?.slice(0, 5),
|
|
216
|
+
activeWorkflowSignalProgress: state.activeWorkflow
|
|
217
|
+
? intelligence.getWorkflowSignalProgress(state.activeWorkflow, state)
|
|
218
|
+
: null
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
return format.error(`Unknown action: ${action}`, [
|
|
223
|
+
'Valid actions: analyze, suggest, recommend, workflows, workflow, start, next, checkpoint, status'
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
getToolDefinition,
|
|
231
|
+
createHandler
|
|
232
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP plugin tool module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function getToolDefinition() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'bootspring_plugin',
|
|
8
|
+
description: 'Manage plugins. List, enable, disable, or show plugin details.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
action: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['list', 'show', 'enable', 'disable'],
|
|
15
|
+
description: 'Action: list, show, enable, or disable'
|
|
16
|
+
},
|
|
17
|
+
name: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'Plugin name (e.g., auth, payments, database)'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: ['action']
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createHandler({ config }) {
|
|
28
|
+
return async (args) => {
|
|
29
|
+
const { action, name } = args;
|
|
30
|
+
const cfg = config.load();
|
|
31
|
+
|
|
32
|
+
switch (action) {
|
|
33
|
+
case 'list': {
|
|
34
|
+
const plugins = Object.entries(cfg.plugins || {}).map(([id, plugin]) => ({
|
|
35
|
+
id,
|
|
36
|
+
enabled: plugin.enabled !== false,
|
|
37
|
+
provider: plugin.provider || 'default'
|
|
38
|
+
}));
|
|
39
|
+
return {
|
|
40
|
+
content: [{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text: JSON.stringify(plugins, null, 2)
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
case 'show': {
|
|
47
|
+
if (!name) throw new Error('Plugin name required');
|
|
48
|
+
const plugin = cfg.plugins?.[name];
|
|
49
|
+
if (!plugin) throw new Error(`Plugin not found: ${name}`);
|
|
50
|
+
return {
|
|
51
|
+
content: [{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: JSON.stringify({ id: name, ...plugin }, null, 2)
|
|
54
|
+
}]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
case 'enable':
|
|
58
|
+
case 'disable': {
|
|
59
|
+
if (!name) throw new Error('Plugin name required');
|
|
60
|
+
return {
|
|
61
|
+
content: [{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: `Plugin ${name} ${action}d. Update bootspring.config.js to persist.`
|
|
64
|
+
}]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
throw new Error(`Unknown action: ${action}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
getToolDefinition,
|
|
75
|
+
createHandler
|
|
76
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP quality tool module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function getToolDefinition() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'bootspring_quality',
|
|
8
|
+
description: 'Run quality gates to check code quality and security.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
gate: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
enum: ['pre-commit', 'pre-push', 'pre-deploy'],
|
|
15
|
+
description: 'Quality gate to run'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
required: ['gate']
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createHandler({ config, quality, format }) {
|
|
24
|
+
return async (args) => {
|
|
25
|
+
const { gate } = args;
|
|
26
|
+
const cfg = config.load();
|
|
27
|
+
const projectRoot = cfg._projectRoot;
|
|
28
|
+
|
|
29
|
+
const availableGates = quality.listGates();
|
|
30
|
+
if (!availableGates.includes(gate)) {
|
|
31
|
+
return format.error(`Unknown gate: ${gate}`, [
|
|
32
|
+
`Available gates: ${availableGates.join(', ')}`
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const results = quality.runGate(gate, projectRoot, {
|
|
37
|
+
verbose: false
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return format.qualityResults(results);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
getToolDefinition,
|
|
46
|
+
createHandler
|
|
47
|
+
};
|