molecule-mcp 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.
- package/README.md +23 -5
- package/assets/conventions.md +31 -0
- package/dist/package-doc-files.d.ts +15 -0
- package/dist/package-doc-files.js +121 -0
- package/dist/package-doc-search.d.ts +20 -0
- package/dist/package-doc-search.js +199 -0
- package/dist/package-search.d.ts +171 -0
- package/dist/package-search.js +241 -0
- package/dist/remote.js +41 -41
- package/dist/scaffold-args.d.ts +12 -0
- package/dist/scaffold-args.js +17 -0
- package/dist/server.js +643 -98
- package/dist/services.d.ts +23 -5
- package/dist/services.js +148 -50
- package/package.json +16 -6
package/dist/services.d.ts
CHANGED
|
@@ -4,9 +4,21 @@ export interface ServiceKey {
|
|
|
4
4
|
source: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* The project API key for service calls
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The project API key for service calls, resolved in this documented order:
|
|
8
|
+
*
|
|
9
|
+
* 1. `MOLECULE_API_KEY` in this server's environment (always wins);
|
|
10
|
+
* 2. `.env`, then every `.env.<variant>` (byte-sorted), in `directory` —
|
|
11
|
+
* then the same set in `directory/api`;
|
|
12
|
+
* 3. the same two locations in each parent, up to two parents.
|
|
13
|
+
*
|
|
14
|
+
* Inside one file, Node's dotenv rules apply: the LAST assignment wins, and
|
|
15
|
+
* inline comments / quoting are honoured. A file that does not define a
|
|
16
|
+
* non-blank MOLECULE_API_KEY is skipped, and placeholder variants
|
|
17
|
+
* (`.env.example`, `.env.schema`, …) are never read. Because `create_api_key`
|
|
18
|
+
* writes only these names through the same predicate, a key it writes at
|
|
19
|
+
* `<dir>/.env[.<variant>]` or `<dir>/api/.env[.<variant>]` is found by a later
|
|
20
|
+
* service call whose `directory` is that project directory (or up to two
|
|
21
|
+
* levels below it).
|
|
10
22
|
*
|
|
11
23
|
* @param directory - Project directory to search (default cwd).
|
|
12
24
|
* @returns The key and where it came from, or null.
|
|
@@ -48,8 +60,14 @@ export interface CreatedKey {
|
|
|
48
60
|
}
|
|
49
61
|
/**
|
|
50
62
|
* Mint a project API key and write it into a dotenv file as
|
|
51
|
-
* MOLECULE_API_KEY, replacing an existing
|
|
52
|
-
* the platform exactly once and is never
|
|
63
|
+
* MOLECULE_API_KEY, replacing an existing assignment and dropping duplicate
|
|
64
|
+
* ones. The plaintext is returned by the platform exactly once and is never
|
|
65
|
+
* echoed back to the agent.
|
|
66
|
+
*
|
|
67
|
+
* The destination is validated BEFORE the platform is asked to mint anything,
|
|
68
|
+
* and only names `resolveServiceKey` reads are accepted (`.env` or
|
|
69
|
+
* `.env.<variant>`, never a placeholder like `.env.example`) — so a written
|
|
70
|
+
* key is always discoverable by the service tools that follow.
|
|
53
71
|
*
|
|
54
72
|
* @param projectId - The project the key bills to.
|
|
55
73
|
* @param name - A label shown in the project's key list.
|
package/dist/services.js
CHANGED
|
@@ -6,28 +6,116 @@
|
|
|
6
6
|
* projects, mint a project API key;
|
|
7
7
|
* - a PROJECT API key (`mk_…`) pays for service calls, metered and billed to
|
|
8
8
|
* that project exactly like a deployed app calling the same URL. It is read
|
|
9
|
-
* from MOLECULE_API_KEY, else from the project's `.env
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* from MOLECULE_API_KEY, else from the project's dotenv files (`.env`, then
|
|
10
|
+
* `.env.<variant>`, in the project root and its `api/` directory — the same
|
|
11
|
+
* place the app's `@molecule/*-molecule` bonds read it), so the key never
|
|
12
|
+
* has to pass through the chat transcript.
|
|
12
13
|
*/
|
|
13
|
-
import {
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { chmodSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
16
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { parseEnv } from 'node:util';
|
|
15
18
|
import { apiBase, sessionToken } from './auth.js';
|
|
16
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The dotenv file names this server treats as project env files — the names
|
|
21
|
+
* `create_api_key` may write and the names key resolution reads. The SAME
|
|
22
|
+
* predicate on both sides is what keeps a written key discoverable.
|
|
23
|
+
*/
|
|
24
|
+
const ENV_FILE_NAME_RE = /^\.env(?:\.([A-Za-z0-9_-]+))?$/;
|
|
25
|
+
/**
|
|
26
|
+
* Variants that hold placeholders or tool bookkeeping, never credentials
|
|
27
|
+
* (`.env.example`, `.env.schema`, `.env.vault`, …). Auto-loading one would
|
|
28
|
+
* send a placeholder as the bearer key, so neither side accepts them.
|
|
29
|
+
* Compared case-insensitively.
|
|
30
|
+
*/
|
|
31
|
+
const PLACEHOLDER_ENV_VARIANTS = new Set([
|
|
32
|
+
'example',
|
|
33
|
+
'sample',
|
|
34
|
+
'schema',
|
|
35
|
+
'template',
|
|
36
|
+
'tmpl',
|
|
37
|
+
'dist',
|
|
38
|
+
'vault',
|
|
39
|
+
'keys',
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Whether a dotenv basename is a project env file: `.env`, or `.env.<variant>`
|
|
43
|
+
* where the variant is not a placeholder.
|
|
44
|
+
*
|
|
45
|
+
* @param name - A dotenv file's basename.
|
|
46
|
+
* @returns True when the file may hold — and be written with — the project key.
|
|
47
|
+
*/
|
|
48
|
+
function isProjectEnvFileName(name) {
|
|
49
|
+
const match = ENV_FILE_NAME_RE.exec(name);
|
|
50
|
+
if (!match)
|
|
51
|
+
return false;
|
|
52
|
+
return match[1] === undefined || !PLACEHOLDER_ENV_VARIANTS.has(match[1].toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse one variable out of a dotenv file with Node's own dotenv parser
|
|
56
|
+
* (`util.parseEnv`), so inline comments, single/double quoting, `export`
|
|
57
|
+
* prefixes, duplicate assignments (last wins) and multi-line quoted values
|
|
58
|
+
* follow exactly the rules `node --env-file` applies — not a lookalike regex.
|
|
59
|
+
*
|
|
60
|
+
* @param file - Dotenv file to read.
|
|
61
|
+
* @param name - Variable name.
|
|
62
|
+
* @returns The parsed non-blank value, or undefined when absent.
|
|
63
|
+
*/
|
|
17
64
|
function readDotenvVar(file, name) {
|
|
18
65
|
if (!existsSync(file))
|
|
19
66
|
return undefined;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
67
|
+
// A BOM would otherwise become part of the first variable's NAME.
|
|
68
|
+
const content = readFileSync(file, 'utf8');
|
|
69
|
+
const parsed = parseEnv(content.charCodeAt(0) === 0xfeff ? content.slice(1) : content);
|
|
70
|
+
const value = parsed[name];
|
|
71
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The dotenv files in one directory that may hold the project key, in
|
|
75
|
+
* precedence order: `.env` first, then `.env.<variant>` byte-sorted — a fixed
|
|
76
|
+
* order across filesystems, so the same tree always resolves the same file.
|
|
77
|
+
*
|
|
78
|
+
* @param dir - Directory to list.
|
|
79
|
+
* @returns Candidate paths (empty when `dir` is not a readable directory).
|
|
80
|
+
*/
|
|
81
|
+
function dotenvCandidates(dir) {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = readdirSync(dir);
|
|
85
|
+
}
|
|
86
|
+
catch (_error) {
|
|
87
|
+
// Not a directory, or not readable: this location has no candidates.
|
|
88
|
+
return [];
|
|
24
89
|
}
|
|
25
|
-
return
|
|
90
|
+
return entries
|
|
91
|
+
.filter(isProjectEnvFileName)
|
|
92
|
+
.sort((a, b) => {
|
|
93
|
+
if (a === b)
|
|
94
|
+
return 0;
|
|
95
|
+
if (a === '.env')
|
|
96
|
+
return -1;
|
|
97
|
+
if (b === '.env')
|
|
98
|
+
return 1;
|
|
99
|
+
return a < b ? -1 : 1;
|
|
100
|
+
})
|
|
101
|
+
.map((name) => join(dir, name));
|
|
26
102
|
}
|
|
27
103
|
/**
|
|
28
|
-
* The project API key for service calls
|
|
29
|
-
*
|
|
30
|
-
*
|
|
104
|
+
* The project API key for service calls, resolved in this documented order:
|
|
105
|
+
*
|
|
106
|
+
* 1. `MOLECULE_API_KEY` in this server's environment (always wins);
|
|
107
|
+
* 2. `.env`, then every `.env.<variant>` (byte-sorted), in `directory` —
|
|
108
|
+
* then the same set in `directory/api`;
|
|
109
|
+
* 3. the same two locations in each parent, up to two parents.
|
|
110
|
+
*
|
|
111
|
+
* Inside one file, Node's dotenv rules apply: the LAST assignment wins, and
|
|
112
|
+
* inline comments / quoting are honoured. A file that does not define a
|
|
113
|
+
* non-blank MOLECULE_API_KEY is skipped, and placeholder variants
|
|
114
|
+
* (`.env.example`, `.env.schema`, …) are never read. Because `create_api_key`
|
|
115
|
+
* writes only these names through the same predicate, a key it writes at
|
|
116
|
+
* `<dir>/.env[.<variant>]` or `<dir>/api/.env[.<variant>]` is found by a later
|
|
117
|
+
* service call whose `directory` is that project directory (or up to two
|
|
118
|
+
* levels below it).
|
|
31
119
|
*
|
|
32
120
|
* @param directory - Project directory to search (default cwd).
|
|
33
121
|
* @returns The key and where it came from, or null.
|
|
@@ -37,11 +125,13 @@ export function resolveServiceKey(directory) {
|
|
|
37
125
|
return { key: process.env.MOLECULE_API_KEY, source: 'MOLECULE_API_KEY env' };
|
|
38
126
|
}
|
|
39
127
|
let dir = resolve(directory ?? process.cwd());
|
|
40
|
-
for (let
|
|
41
|
-
for (const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
128
|
+
for (let level = 0; level < 3; level++) {
|
|
129
|
+
for (const location of [dir, join(dir, 'api')]) {
|
|
130
|
+
for (const file of dotenvCandidates(location)) {
|
|
131
|
+
const key = readDotenvVar(file, 'MOLECULE_API_KEY');
|
|
132
|
+
if (key)
|
|
133
|
+
return { key, source: file };
|
|
134
|
+
}
|
|
45
135
|
}
|
|
46
136
|
const parent = dirname(dir);
|
|
47
137
|
if (parent === dir)
|
|
@@ -83,7 +173,7 @@ function retryHint(seconds, errorKey) {
|
|
|
83
173
|
export async function callHostedService(service, operation, body, directory) {
|
|
84
174
|
const found = resolveServiceKey(directory);
|
|
85
175
|
if (!found) {
|
|
86
|
-
throw new ServiceCallError('No molecule project API key. Call create_api_key (it writes MOLECULE_API_KEY into the project .env), or set MOLECULE_API_KEY for this server.');
|
|
176
|
+
throw new ServiceCallError('No molecule project API key. Call create_api_key (it writes MOLECULE_API_KEY into the project .env or .env.<name>), or set MOLECULE_API_KEY for this server.');
|
|
87
177
|
}
|
|
88
178
|
const response = await fetch(`${apiBase()}/v1/services/${service}/${operation}`, {
|
|
89
179
|
method: 'POST',
|
|
@@ -156,12 +246,16 @@ export async function listProjects() {
|
|
|
156
246
|
return { id: p.id, name: p.name, ...((p.role ?? r.role) ? { role: p.role ?? r.role } : {}) };
|
|
157
247
|
});
|
|
158
248
|
}
|
|
159
|
-
/** The only files create_api_key will write: a dotenv file by name. */
|
|
160
|
-
const ENV_FILE_RE = /^\.env(\.[A-Za-z0-9_-]+)?$/;
|
|
161
249
|
/**
|
|
162
250
|
* Mint a project API key and write it into a dotenv file as
|
|
163
|
-
* MOLECULE_API_KEY, replacing an existing
|
|
164
|
-
* the platform exactly once and is never
|
|
251
|
+
* MOLECULE_API_KEY, replacing an existing assignment and dropping duplicate
|
|
252
|
+
* ones. The plaintext is returned by the platform exactly once and is never
|
|
253
|
+
* echoed back to the agent.
|
|
254
|
+
*
|
|
255
|
+
* The destination is validated BEFORE the platform is asked to mint anything,
|
|
256
|
+
* and only names `resolveServiceKey` reads are accepted (`.env` or
|
|
257
|
+
* `.env.<variant>`, never a placeholder like `.env.example`) — so a written
|
|
258
|
+
* key is always discoverable by the service tools that follow.
|
|
165
259
|
*
|
|
166
260
|
* @param projectId - The project the key bills to.
|
|
167
261
|
* @param name - A label shown in the project's key list.
|
|
@@ -171,8 +265,8 @@ const ENV_FILE_RE = /^\.env(\.[A-Za-z0-9_-]+)?$/;
|
|
|
171
265
|
*/
|
|
172
266
|
export async function createApiKeyIntoEnv(projectId, name, envFile, scopes) {
|
|
173
267
|
const target = resolve(envFile);
|
|
174
|
-
if (!
|
|
175
|
-
throw new ServiceCallError('env_file must be a dotenv file named .env or .env.<name
|
|
268
|
+
if (!isProjectEnvFileName(basename(target))) {
|
|
269
|
+
throw new ServiceCallError('env_file must be a dotenv file named .env or .env.<name>; placeholder files such as .env.example are never read for credentials.');
|
|
176
270
|
}
|
|
177
271
|
if (!existsSync(dirname(target))) {
|
|
178
272
|
throw new ServiceCallError(`Directory ${dirname(target)} does not exist.`);
|
|
@@ -186,33 +280,37 @@ export async function createApiKeyIntoEnv(projectId, name, envFile, scopes) {
|
|
|
186
280
|
const line = `MOLECULE_API_KEY=${minted.token}`;
|
|
187
281
|
const existed = existsSync(target);
|
|
188
282
|
const lines = existed ? readFileSync(target, 'utf8').split('\n') : [];
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
283
|
+
// Rewrite the FIRST assignment and drop any later ones: Node's dotenv parser
|
|
284
|
+
// takes the LAST assignment in a file, so leaving an older line below the new
|
|
285
|
+
// one would keep the superseded key in force and every service call would use
|
|
286
|
+
// it. Only assignment lines are matched (a commented-out `# KEY=` is not one).
|
|
287
|
+
const assignment = /^\s*(?:export\s+)?MOLECULE_API_KEY\s*=/;
|
|
288
|
+
const next = [];
|
|
289
|
+
let written = false;
|
|
290
|
+
for (const current of lines) {
|
|
291
|
+
if (!assignment.test(current)) {
|
|
292
|
+
next.push(current);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!written) {
|
|
296
|
+
next.push(line);
|
|
297
|
+
written = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!written) {
|
|
301
|
+
if (next.length && next[next.length - 1] === '')
|
|
302
|
+
next.pop();
|
|
303
|
+
next.push(line, '');
|
|
304
|
+
}
|
|
305
|
+
writeFileSync(target, next.join('\n'), { mode: 0o600 });
|
|
198
306
|
// The file now holds a secret: owner-only even if it existed with wider bits.
|
|
199
307
|
chmodSync(target, 0o600);
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
gitignored =
|
|
207
|
-
existsSync(gitignore) &&
|
|
208
|
-
readFileSync(gitignore, 'utf8')
|
|
209
|
-
.split('\n')
|
|
210
|
-
.some((l) => patterns.includes(l.trim()));
|
|
211
|
-
const parent = dirname(dir);
|
|
212
|
-
if (parent === dir)
|
|
213
|
-
break;
|
|
214
|
-
dir = parent;
|
|
215
|
-
}
|
|
308
|
+
// Ask Git, including negations, nested rules and tracked-file status. A failed
|
|
309
|
+
// or unavailable Git check never claims that a secret is excluded.
|
|
310
|
+
const gitignored = spawnSync('git', ['check-ignore', '--quiet', '--', target], {
|
|
311
|
+
cwd: dirname(target),
|
|
312
|
+
stdio: 'ignore',
|
|
313
|
+
}).status === 0;
|
|
216
314
|
return {
|
|
217
315
|
id: minted.id,
|
|
218
316
|
masked: minted.masked,
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "molecule-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server exposing the Molecule.dev package ecosystem — search, templates, scaffolding — to any MCP-capable coding agent",
|
|
5
5
|
"author": "Molecule Dev, Inc. (https://molecule.dev)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"homepage": "https://www.molecule.dev/mlcl",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/molecule-dev/mlcl.git",
|
|
11
|
+
"directory": "mcp"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/molecule-dev/mlcl/issues"
|
|
15
|
+
},
|
|
7
16
|
"type": "module",
|
|
8
17
|
"engines": {
|
|
9
18
|
"node": ">=22"
|
|
@@ -20,18 +29,19 @@
|
|
|
20
29
|
],
|
|
21
30
|
"scripts": {
|
|
22
31
|
"build": "shx rm -rf dist && tsc -b",
|
|
32
|
+
"gen:package-search": "node ../scripts/generate-mcp-package-search.mjs",
|
|
23
33
|
"smoke": "node scripts/smoke.mjs",
|
|
24
34
|
"serve": "node dist/http.js"
|
|
25
35
|
},
|
|
26
36
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "1.
|
|
28
|
-
"execa": "10.
|
|
29
|
-
"zod": "
|
|
30
|
-
"express": "5.
|
|
37
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
38
|
+
"execa": "10.2.1",
|
|
39
|
+
"zod": "4.6.7",
|
|
40
|
+
"express": "5.3.0"
|
|
31
41
|
},
|
|
32
42
|
"devDependencies": {
|
|
33
43
|
"@types/express": "5.0.6",
|
|
34
|
-
"@types/node": "26.
|
|
44
|
+
"@types/node": "26.6.5",
|
|
35
45
|
"shx": "0.4.0",
|
|
36
46
|
"typescript": "6.0.3"
|
|
37
47
|
}
|