stellar-agent 0.2.0 → 0.3.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.
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stellar Agent — MCP Server
4
+ *
5
+ * Exposes the stellar-agent skill catalog and agent personas to any
6
+ * MCP-compatible client (Claude Desktop, Cline, Continue, Zed, etc.)
7
+ * over stdio.
8
+ *
9
+ * Hand-rolled JSON-RPC 2.0 implementation to avoid an SDK dependency —
10
+ * keeps the published package lean.
11
+ *
12
+ * Tools exposed:
13
+ * - list_agents → roster of all 8 agents
14
+ * - list_skills(phase?, agent?) → catalog of skills
15
+ * - get_skill(name) → SKILL.md content
16
+ * - get_agent(name) → persona block + menu
17
+ *
18
+ * Run: `stellar-agent mcp` or `node tools/mcp-server/server.js`
19
+ */
20
+
21
+ const fsp = require('node:fs/promises');
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+ const readline = require('node:readline');
25
+ const yaml = require('yaml');
26
+
27
+ const REPO_ROOT = path.join(__dirname, '..', '..');
28
+ const SRC_DIR = path.join(REPO_ROOT, 'src');
29
+ const STELLAR_SKILLS_DIR = path.join(SRC_DIR, 'stellar-skills');
30
+ const CORE_SKILLS_DIR = path.join(SRC_DIR, 'core-skills');
31
+ const PROTOCOL_VERSION = '2024-11-05';
32
+ const SERVER_INFO = { name: 'stellar-agent', version: require('../../package.json').version };
33
+
34
+ // ---------- Catalog loaders ----------
35
+
36
+ async function loadAgents() {
37
+ const moduleYaml = await fsp.readFile(path.join(STELLAR_SKILLS_DIR, 'module.yaml'), 'utf8');
38
+ const data = yaml.parse(moduleYaml);
39
+ return data.agents || [];
40
+ }
41
+
42
+ async function loadSkills({ phase, agent } = {}) {
43
+ const skills = [];
44
+
45
+ // Walk core-skills/*/SKILL.md
46
+ for (const entry of await fsp.readdir(CORE_SKILLS_DIR, { withFileTypes: true })) {
47
+ if (!entry.isDirectory()) continue;
48
+ const skillPath = path.join(CORE_SKILLS_DIR, entry.name, 'SKILL.md');
49
+ if (!fs.existsSync(skillPath)) continue;
50
+ skills.push(await readSkillFrontmatter(skillPath, 'core', entry.name));
51
+ }
52
+
53
+ // Walk stellar-skills/<phase>/<skill>/SKILL.md
54
+ for (const phaseEntry of await fsp.readdir(STELLAR_SKILLS_DIR, { withFileTypes: true })) {
55
+ if (!phaseEntry.isDirectory()) continue;
56
+ const phaseDir = path.join(STELLAR_SKILLS_DIR, phaseEntry.name);
57
+ for (const skillEntry of await fsp.readdir(phaseDir, { withFileTypes: true })) {
58
+ if (!skillEntry.isDirectory()) continue;
59
+ const skillPath = path.join(phaseDir, skillEntry.name, 'SKILL.md');
60
+ if (!fs.existsSync(skillPath)) continue;
61
+ skills.push(await readSkillFrontmatter(skillPath, phaseEntry.name, skillEntry.name));
62
+ }
63
+ }
64
+
65
+ let filtered = skills;
66
+ if (phase) filtered = filtered.filter((s) => s.phase === phase);
67
+ if (agent) filtered = filtered.filter((s) => (s.agent || '').toLowerCase() === agent.toLowerCase());
68
+ return filtered;
69
+ }
70
+
71
+ async function readSkillFrontmatter(filePath, phase, dirname) {
72
+ const raw = await fsp.readFile(filePath, 'utf8');
73
+ const frontmatter = parseFrontmatter(raw);
74
+ // Detect agent-persona skills by naming convention: stellar-agent-*
75
+ const isAgent = dirname.startsWith('stellar-agent-');
76
+ return {
77
+ name: frontmatter.name || dirname,
78
+ description: frontmatter.description || '',
79
+ phase,
80
+ type: isAgent ? 'agent' : 'skill',
81
+ path: path.relative(REPO_ROOT, filePath),
82
+ };
83
+ }
84
+
85
+ function parseFrontmatter(text) {
86
+ const match = text.match(/^---\n([\s\S]*?)\n---/);
87
+ if (!match) return {};
88
+ const out = {};
89
+ for (const line of match[1].split('\n')) {
90
+ const m = line.match(/^([a-zA-Z_]+):\s*['"]?(.*?)['"]?$/);
91
+ if (m) out[m[1]] = m[2];
92
+ }
93
+ return out;
94
+ }
95
+
96
+ async function getSkillContent(name) {
97
+ // Look in core-skills
98
+ const corePath = path.join(CORE_SKILLS_DIR, name, 'SKILL.md');
99
+ if (fs.existsSync(corePath)) return fsp.readFile(corePath, 'utf8');
100
+ // Look in stellar-skills/<phase>/<name>
101
+ for (const phaseEntry of await fsp.readdir(STELLAR_SKILLS_DIR, { withFileTypes: true })) {
102
+ if (!phaseEntry.isDirectory()) continue;
103
+ const candidate = path.join(STELLAR_SKILLS_DIR, phaseEntry.name, name, 'SKILL.md');
104
+ if (fs.existsSync(candidate)) return fsp.readFile(candidate, 'utf8');
105
+ }
106
+ throw new Error(`Skill not found: ${name}`);
107
+ }
108
+
109
+ async function getAgentDetail(name) {
110
+ const agents = await loadAgents();
111
+ const agent = agents.find(
112
+ (a) =>
113
+ a.code === name ||
114
+ a.name?.toLowerCase() === name.toLowerCase() ||
115
+ a.code === `stellar-agent-${name.toLowerCase()}`
116
+ );
117
+ if (!agent) throw new Error(`Agent not found: ${name}`);
118
+
119
+ // Resolve agent's SKILL.md + customize.toml for menu
120
+ let menu = [];
121
+ let persona = null;
122
+ for (const phaseEntry of await fsp.readdir(STELLAR_SKILLS_DIR, { withFileTypes: true })) {
123
+ if (!phaseEntry.isDirectory()) continue;
124
+ const dir = path.join(STELLAR_SKILLS_DIR, phaseEntry.name, agent.code);
125
+ if (!fs.existsSync(dir)) continue;
126
+ const skillPath = path.join(dir, 'SKILL.md');
127
+ const customizePath = path.join(dir, 'customize.toml');
128
+ if (fs.existsSync(skillPath)) persona = await fsp.readFile(skillPath, 'utf8');
129
+ if (fs.existsSync(customizePath)) {
130
+ const tomlText = await fsp.readFile(customizePath, 'utf8');
131
+ menu = parseAgentMenu(tomlText);
132
+ }
133
+ break;
134
+ }
135
+ return { agent, persona, menu };
136
+ }
137
+
138
+ function parseAgentMenu(toml) {
139
+ // Lightweight extractor for [[agent.menu]] blocks
140
+ const blocks = toml.split(/\[\[agent\.menu\]\]/).slice(1);
141
+ return blocks.map((block) => {
142
+ const lines = block.split('\n');
143
+ const entry = {};
144
+ for (const line of lines) {
145
+ const m = line.match(/^\s*(code|description|skill)\s*=\s*"(.*)"\s*$/);
146
+ if (m) entry[m[1]] = m[2];
147
+ }
148
+ return entry;
149
+ });
150
+ }
151
+
152
+ // ---------- MCP protocol ----------
153
+
154
+ const TOOLS = [
155
+ {
156
+ name: 'list_agents',
157
+ description: 'List all Stellar Agent team members with their handle, role, icon, and phase.',
158
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
159
+ },
160
+ {
161
+ name: 'list_skills',
162
+ description:
163
+ 'List available skills in the Stellar Agent catalog. Optionally filter by phase (1-analysis, 2-planning, 3-architecture, 4-implementation, 5-mentorship, core).',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: {
167
+ phase: {
168
+ type: 'string',
169
+ description: 'Optional phase filter — e.g. "4-implementation" or "core".',
170
+ },
171
+ },
172
+ additionalProperties: false,
173
+ },
174
+ },
175
+ {
176
+ name: 'get_skill',
177
+ description: 'Return the full SKILL.md content for a named skill (e.g. "stellar-write-contract").',
178
+ inputSchema: {
179
+ type: 'object',
180
+ properties: { name: { type: 'string', description: 'Skill name (kebab-case).' } },
181
+ required: ['name'],
182
+ additionalProperties: false,
183
+ },
184
+ },
185
+ {
186
+ name: 'get_agent',
187
+ description:
188
+ 'Return the persona, principles, and skill menu for a specific Stellar Agent (e.g. "sol", "pera").',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ name: { type: 'string', description: 'Agent name or handle (e.g. "sol" or "stellar-agent-developer").' },
193
+ },
194
+ required: ['name'],
195
+ additionalProperties: false,
196
+ },
197
+ },
198
+ ];
199
+
200
+ function jsonResponse(id, result) {
201
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
202
+ }
203
+
204
+ function jsonError(id, code, message) {
205
+ return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
206
+ }
207
+
208
+ async function handleRequest(req) {
209
+ const { id, method, params } = req;
210
+
211
+ if (method === 'initialize') {
212
+ return jsonResponse(id, {
213
+ protocolVersion: PROTOCOL_VERSION,
214
+ capabilities: { tools: {} },
215
+ serverInfo: SERVER_INFO,
216
+ });
217
+ }
218
+
219
+ if (method === 'tools/list') {
220
+ return jsonResponse(id, { tools: TOOLS });
221
+ }
222
+
223
+ if (method === 'tools/call') {
224
+ const { name, arguments: args = {} } = params || {};
225
+ try {
226
+ let payload;
227
+ if (name === 'list_agents') {
228
+ payload = await loadAgents();
229
+ } else if (name === 'list_skills') {
230
+ payload = await loadSkills(args);
231
+ } else if (name === 'get_skill') {
232
+ if (!args.name) throw new Error('Missing required argument: name');
233
+ payload = await getSkillContent(args.name);
234
+ } else if (name === 'get_agent') {
235
+ if (!args.name) throw new Error('Missing required argument: name');
236
+ payload = await getAgentDetail(args.name);
237
+ } else {
238
+ return jsonError(id, -32601, `Unknown tool: ${name}`);
239
+ }
240
+
241
+ const text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
242
+ return jsonResponse(id, { content: [{ type: 'text', text }] });
243
+ } catch (err) {
244
+ return jsonResponse(id, {
245
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
246
+ isError: true,
247
+ });
248
+ }
249
+ }
250
+
251
+ if (method === 'notifications/initialized') {
252
+ return null; // no response for notifications
253
+ }
254
+
255
+ return jsonError(id, -32601, `Method not found: ${method}`);
256
+ }
257
+
258
+ // ---------- Stdio loop ----------
259
+
260
+ async function main() {
261
+ const rl = readline.createInterface({ input: process.stdin });
262
+ rl.on('line', async (line) => {
263
+ if (!line.trim()) return;
264
+ let req;
265
+ try {
266
+ req = JSON.parse(line);
267
+ } catch (err) {
268
+ process.stdout.write(jsonError(null, -32700, 'Parse error') + '\n');
269
+ return;
270
+ }
271
+ const response = await handleRequest(req);
272
+ if (response) process.stdout.write(response + '\n');
273
+ });
274
+
275
+ rl.on('close', () => process.exit(0));
276
+ }
277
+
278
+ main().catch((err) => {
279
+ console.error('MCP server fatal error:', err);
280
+ process.exit(1);
281
+ });