@openephemeris/mcp-server 3.1.0 → 3.2.2
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 +51 -22
- package/config/dev-allowlist.json +1262 -1318
- package/dist/index.js +0 -0
- package/dist/scripts/dev-allowlist.d.ts +1 -0
- package/dist/scripts/dev-allowlist.js +287 -0
- package/dist/scripts/pack-audit.d.ts +1 -0
- package/dist/scripts/pack-audit.js +45 -0
- package/dist/scripts/schema-packs.d.ts +1 -0
- package/dist/scripts/schema-packs.js +150 -0
- package/dist/scripts/smoke-dev-profile.d.ts +1 -0
- package/dist/scripts/smoke-dev-profile.js +25 -0
- package/dist/scripts/sync-readme.d.ts +1 -0
- package/dist/scripts/sync-readme.js +141 -0
- package/dist/scripts/test-client.d.ts +1 -0
- package/dist/scripts/test-client.js +69 -0
- package/dist/scripts/test-sse-client.d.ts +1 -0
- package/dist/scripts/test-sse-client.js +221 -0
- package/dist/src/auth/credentials.d.ts +65 -0
- package/dist/src/auth/credentials.js +200 -0
- package/dist/src/auth/device-auth.d.ts +56 -0
- package/dist/src/auth/device-auth.js +147 -0
- package/dist/src/backend/client.d.ts +61 -0
- package/dist/src/backend/client.js +335 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +98 -0
- package/dist/src/schema-packs/llm.d.ts +105 -0
- package/dist/src/schema-packs/llm.js +429 -0
- package/dist/src/server-sse.d.ts +1 -0
- package/dist/src/server-sse.js +264 -0
- package/dist/src/tools/auth.d.ts +1 -0
- package/dist/src/tools/auth.js +202 -0
- package/dist/src/tools/dev.d.ts +1 -0
- package/dist/src/tools/dev.js +195 -0
- package/dist/src/tools/index.d.ts +33 -0
- package/dist/src/tools/index.js +56 -0
- package/dist/src/tools/specialized/eclipse.d.ts +1 -0
- package/dist/src/tools/specialized/eclipse.js +53 -0
- package/dist/src/tools/specialized/electional.d.ts +1 -0
- package/dist/src/tools/specialized/electional.js +80 -0
- package/dist/src/tools/specialized/human_design.d.ts +1 -0
- package/dist/src/tools/specialized/human_design.js +54 -0
- package/dist/src/tools/specialized/moon.d.ts +1 -0
- package/dist/src/tools/specialized/moon.js +51 -0
- package/dist/src/tools/specialized/natal.d.ts +1 -0
- package/dist/src/tools/specialized/natal.js +80 -0
- package/dist/src/tools/specialized/relocation.d.ts +1 -0
- package/dist/src/tools/specialized/relocation.js +76 -0
- package/dist/src/tools/specialized/synastry.d.ts +1 -0
- package/dist/src/tools/specialized/synastry.js +73 -0
- package/dist/src/tools/specialized/transits.d.ts +1 -0
- package/dist/src/tools/specialized/transits.js +87 -0
- package/dist/test/allowlist-and-tools.test.d.ts +1 -0
- package/dist/test/allowlist-and-tools.test.js +96 -0
- package/dist/test/backend-client.test.d.ts +1 -0
- package/dist/test/backend-client.test.js +284 -0
- package/dist/test/credentials.test.d.ts +1 -0
- package/dist/test/credentials.test.js +143 -0
- package/package.json +27 -18
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
function normalizeTier(raw) {
|
|
7
|
+
if (typeof raw !== "string")
|
|
8
|
+
return null;
|
|
9
|
+
const value = raw.trim().toLowerCase();
|
|
10
|
+
if (value === "free" ||
|
|
11
|
+
value === "all" ||
|
|
12
|
+
value === "free/all" ||
|
|
13
|
+
value === "anonymous" ||
|
|
14
|
+
value === "anonymous/all" ||
|
|
15
|
+
value === "public" ||
|
|
16
|
+
value === "public/all")
|
|
17
|
+
return "free";
|
|
18
|
+
if (value === "dev" || value === "developer")
|
|
19
|
+
return "developer";
|
|
20
|
+
if (value === "pro")
|
|
21
|
+
return "pro";
|
|
22
|
+
if (value === "production")
|
|
23
|
+
return "production";
|
|
24
|
+
if (value === "admin" || value === "internal")
|
|
25
|
+
return "admin";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function isCommercialTier(tierNormalized) {
|
|
29
|
+
// Dev MCP should expose all commercially available tiers up through production.
|
|
30
|
+
return tierNormalized === "free" || tierNormalized === "developer" || tierNormalized === "pro" || tierNormalized === "production";
|
|
31
|
+
}
|
|
32
|
+
function sha256(text) {
|
|
33
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
function readJsonFile(filePath) {
|
|
36
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
return { raw, json: JSON.parse(raw) };
|
|
38
|
+
}
|
|
39
|
+
function writeJsonFile(filePath, obj) {
|
|
40
|
+
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2) + "\n", "utf-8");
|
|
41
|
+
}
|
|
42
|
+
function normalizeTag(tag) {
|
|
43
|
+
return tag.trim().toLowerCase();
|
|
44
|
+
}
|
|
45
|
+
function isDeniedByPrefix(pathname, prefixes) {
|
|
46
|
+
return prefixes.some((p) => pathname.startsWith(p));
|
|
47
|
+
}
|
|
48
|
+
function hasDeniedTag(tags, denyTags) {
|
|
49
|
+
const deny = new Set(denyTags.map(normalizeTag));
|
|
50
|
+
return tags.some((t) => deny.has(normalizeTag(t)));
|
|
51
|
+
}
|
|
52
|
+
function getDefaults() {
|
|
53
|
+
const here = path.dirname(fileURLToPath(import.meta.url)); // .../mcp-server/scripts
|
|
54
|
+
const mcpServerRoot = path.resolve(here, "..");
|
|
55
|
+
const repoRoot = path.resolve(mcpServerRoot, "..");
|
|
56
|
+
return {
|
|
57
|
+
mcpServerRoot,
|
|
58
|
+
repoRoot,
|
|
59
|
+
allowlistPath: path.join(mcpServerRoot, "config", "dev-allowlist.json"),
|
|
60
|
+
openapiPath: path.join(repoRoot, "go-sidecar", "openapi.json"),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseArgs(argv) {
|
|
64
|
+
const args = {
|
|
65
|
+
openapiPath: undefined,
|
|
66
|
+
allowlistPath: undefined,
|
|
67
|
+
write: false,
|
|
68
|
+
check: false,
|
|
69
|
+
};
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
const a = argv[i];
|
|
72
|
+
if (a === "--openapi")
|
|
73
|
+
args.openapiPath = argv[++i];
|
|
74
|
+
else if (a === "--allowlist")
|
|
75
|
+
args.allowlistPath = argv[++i];
|
|
76
|
+
else if (a === "--write")
|
|
77
|
+
args.write = true;
|
|
78
|
+
else if (a === "--check")
|
|
79
|
+
args.check = true;
|
|
80
|
+
else if (a === "--help" || a === "-h") {
|
|
81
|
+
console.log([
|
|
82
|
+
"Dev allowlist helper (astromcp-dev-allowlist-v1)",
|
|
83
|
+
"",
|
|
84
|
+
"Usage:",
|
|
85
|
+
" tsx scripts/dev-allowlist.ts --check",
|
|
86
|
+
" tsx scripts/dev-allowlist.ts --write",
|
|
87
|
+
"",
|
|
88
|
+
"Options:",
|
|
89
|
+
" --openapi <path> Path to OpenAPI JSON (default: ../go-sidecar/openapi.json)",
|
|
90
|
+
" --allowlist <path> Path to allowlist JSON (default: config/dev-allowlist.json)",
|
|
91
|
+
" --check Validate allowlist against OpenAPI + deny rules (exit 1 on failure)",
|
|
92
|
+
" --write Update allowlist metadata + candidates + auto-allow commercial-tier operations",
|
|
93
|
+
].join("\n"));
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return args;
|
|
98
|
+
}
|
|
99
|
+
function asHttpMethod(m) {
|
|
100
|
+
const up = m.toUpperCase();
|
|
101
|
+
if (up === "GET" || up === "POST" || up === "PUT" || up === "PATCH" || up === "DELETE")
|
|
102
|
+
return up;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function buildCandidates(openapi, denyPrefixes, denyTags) {
|
|
106
|
+
const candidatesGet = [];
|
|
107
|
+
const candidatesNonGet = [];
|
|
108
|
+
const index = new Map();
|
|
109
|
+
for (const [pathname, pathItem] of Object.entries(openapi.paths || {})) {
|
|
110
|
+
// OpenAPI keys can include templated params; we keep as-is.
|
|
111
|
+
if (denyPrefixes.length && isDeniedByPrefix(pathname, denyPrefixes))
|
|
112
|
+
continue;
|
|
113
|
+
for (const [methodKey, operation] of Object.entries(pathItem || {})) {
|
|
114
|
+
const method = asHttpMethod(methodKey);
|
|
115
|
+
if (!method)
|
|
116
|
+
continue;
|
|
117
|
+
const tags = Array.isArray(operation?.tags) ? operation.tags : [];
|
|
118
|
+
if (denyTags.length && hasDeniedTag(tags, denyTags))
|
|
119
|
+
continue;
|
|
120
|
+
const tier = typeof operation?.["x-meridian-tier"] === "string" ? String(operation["x-meridian-tier"]) : undefined;
|
|
121
|
+
const tier_normalized = tier ? normalizeTier(tier) ?? undefined : undefined;
|
|
122
|
+
const candidate = {
|
|
123
|
+
method,
|
|
124
|
+
path: pathname,
|
|
125
|
+
tags,
|
|
126
|
+
tier,
|
|
127
|
+
tier_normalized,
|
|
128
|
+
operationId: typeof operation?.operationId === "string" ? operation.operationId : undefined,
|
|
129
|
+
summary: typeof operation?.summary === "string" ? operation.summary : undefined,
|
|
130
|
+
};
|
|
131
|
+
const key = `${method} ${pathname}`;
|
|
132
|
+
index.set(key, candidate);
|
|
133
|
+
if (method === "GET")
|
|
134
|
+
candidatesGet.push(candidate);
|
|
135
|
+
else
|
|
136
|
+
candidatesNonGet.push(candidate);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
candidatesGet.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
|
|
140
|
+
candidatesNonGet.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
|
|
141
|
+
return { candidatesGet, candidatesNonGet, index };
|
|
142
|
+
}
|
|
143
|
+
function buildCommercialAllow(candidates, existingAllow) {
|
|
144
|
+
// Build a lookup of existing notes/metadata to preserve across regen
|
|
145
|
+
const existingMeta = new Map();
|
|
146
|
+
for (const e of existingAllow) {
|
|
147
|
+
if (e.note)
|
|
148
|
+
existingMeta.set(`${e.method} ${e.path}`, { note: e.note });
|
|
149
|
+
}
|
|
150
|
+
const allow = candidates
|
|
151
|
+
.filter((c) => isCommercialTier(c.tier_normalized))
|
|
152
|
+
.map((c) => {
|
|
153
|
+
const key = `${c.method} ${c.path}`;
|
|
154
|
+
const entry = { method: c.method, path: c.path };
|
|
155
|
+
const meta = existingMeta.get(key);
|
|
156
|
+
if (meta?.note)
|
|
157
|
+
entry.note = meta.note;
|
|
158
|
+
return entry;
|
|
159
|
+
});
|
|
160
|
+
// Deterministic ordering for diffs.
|
|
161
|
+
allow.sort((a, b) => (a.path + a.method).localeCompare(b.path + b.method));
|
|
162
|
+
return allow;
|
|
163
|
+
}
|
|
164
|
+
function validateAllowlist(allowlist, index, denyPrefixes, denyTags) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
const warnings = [];
|
|
167
|
+
for (const entry of allowlist.allow) {
|
|
168
|
+
if (!entry || typeof entry !== "object") {
|
|
169
|
+
errors.push(`Invalid allow entry (not an object): ${JSON.stringify(entry)}`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const method = entry.method;
|
|
173
|
+
const pathname = entry.path;
|
|
174
|
+
if (!asHttpMethod(method)) {
|
|
175
|
+
errors.push(`Invalid method in allow entry: ${JSON.stringify(entry)}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (typeof pathname !== "string" || !pathname.startsWith("/")) {
|
|
179
|
+
errors.push(`Invalid path in allow entry: ${JSON.stringify(entry)}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (denyPrefixes.length && isDeniedByPrefix(pathname, denyPrefixes)) {
|
|
183
|
+
errors.push(`Allowlisted entry violates deny prefix rule: ${method} ${pathname}`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const key = `${method.toUpperCase()} ${pathname}`;
|
|
187
|
+
const candidate = index.get(key);
|
|
188
|
+
if (!candidate) {
|
|
189
|
+
errors.push(`Allowlisted entry not found in OpenAPI: ${key}`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!isCommercialTier(candidate.tier_normalized)) {
|
|
193
|
+
errors.push(`Allowlisted entry is not a commercial-tier operation: ${key} (x-meridian-tier=${candidate.tier ?? "<missing>"})`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (denyTags.length && hasDeniedTag(candidate.tags, denyTags)) {
|
|
197
|
+
errors.push(`Allowlisted entry violates deny tag rule: ${key} (tags=${candidate.tags.join(",")})`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Method gating reminder: non-GET is allowed only by explicit allowlist (which it is),
|
|
201
|
+
// but we keep a warning so reviewers notice.
|
|
202
|
+
if (method.toUpperCase() !== "GET") {
|
|
203
|
+
warnings.push(`Non-GET allowlisted: ${key}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { errors, warnings };
|
|
207
|
+
}
|
|
208
|
+
async function getOpenApiDocument(inputPathOrUrl) {
|
|
209
|
+
if (inputPathOrUrl.startsWith("http")) {
|
|
210
|
+
console.log(`Fetching OpenAPI from URL: ${inputPathOrUrl}`);
|
|
211
|
+
const response = await axios.get(inputPathOrUrl);
|
|
212
|
+
const json = response.data;
|
|
213
|
+
return { raw: JSON.stringify(json), json };
|
|
214
|
+
}
|
|
215
|
+
const raw = fs.readFileSync(inputPathOrUrl, "utf-8");
|
|
216
|
+
return { raw, json: JSON.parse(raw) };
|
|
217
|
+
}
|
|
218
|
+
async function main() {
|
|
219
|
+
const defaults = getDefaults();
|
|
220
|
+
const args = parseArgs(process.argv.slice(2));
|
|
221
|
+
const allowlistPath = path.resolve(args.allowlistPath || defaults.allowlistPath);
|
|
222
|
+
const openapiPathOrUrl = args.openapiPath || process.env.ASTROMCP_OPENAPI_URL || defaults.openapiPath;
|
|
223
|
+
const { raw: allowRaw, json: allowlist } = readJsonFile(allowlistPath);
|
|
224
|
+
if (!allowlist || allowlist.schema !== "astromcp-dev-allowlist-v1" || !Array.isArray(allowlist.allow)) {
|
|
225
|
+
throw new Error(`Invalid allowlist schema at ${allowlistPath}`);
|
|
226
|
+
}
|
|
227
|
+
const denyPrefixes = allowlist.deny?.path_prefixes ?? [];
|
|
228
|
+
const denyTags = allowlist.deny?.tags ?? [];
|
|
229
|
+
const { raw: openapiRaw, json: openapi } = await getOpenApiDocument(openapiPathOrUrl);
|
|
230
|
+
if (!openapi || typeof openapi !== "object" || !openapi.paths) {
|
|
231
|
+
throw new Error(`Invalid OpenAPI document at ${openapiPathOrUrl}`);
|
|
232
|
+
}
|
|
233
|
+
const openapiHash = sha256(openapiRaw);
|
|
234
|
+
const allowlistHash = sha256(allowRaw);
|
|
235
|
+
const { candidatesGet, candidatesNonGet, index } = buildCandidates(openapi, denyPrefixes, denyTags);
|
|
236
|
+
const computedAllow = buildCommercialAllow([...candidatesGet, ...candidatesNonGet], allowlist.allow);
|
|
237
|
+
const allowlistForValidation = args.write ? { ...allowlist, allow: computedAllow } : allowlist;
|
|
238
|
+
const { errors, warnings } = validateAllowlist(allowlistForValidation, index, denyPrefixes, denyTags);
|
|
239
|
+
const report = {
|
|
240
|
+
ok: errors.length === 0,
|
|
241
|
+
allowlistPath,
|
|
242
|
+
openapiPath: openapiPathOrUrl,
|
|
243
|
+
openapi_sha256: openapiHash,
|
|
244
|
+
allowlist_sha256: allowlistHash,
|
|
245
|
+
counts: {
|
|
246
|
+
allow: allowlistForValidation.allow.length,
|
|
247
|
+
candidates_get: candidatesGet.length,
|
|
248
|
+
candidates_non_get: candidatesNonGet.length,
|
|
249
|
+
errors: errors.length,
|
|
250
|
+
warnings: warnings.length,
|
|
251
|
+
},
|
|
252
|
+
errors,
|
|
253
|
+
warnings,
|
|
254
|
+
};
|
|
255
|
+
if (args.write) {
|
|
256
|
+
const now = new Date().toISOString();
|
|
257
|
+
const updated = {
|
|
258
|
+
...allowlist,
|
|
259
|
+
generated_from: openapiPathOrUrl,
|
|
260
|
+
last_generated_at: now,
|
|
261
|
+
openapi_sha256: openapiHash,
|
|
262
|
+
allow: computedAllow,
|
|
263
|
+
candidates_get: candidatesGet.map((c) => ({ method: c.method, path: c.path, tags: c.tags, operationId: c.operationId })),
|
|
264
|
+
candidates_non_get: candidatesNonGet.map((c) => ({ method: c.method, path: c.path, tags: c.tags, operationId: c.operationId })),
|
|
265
|
+
validation: {
|
|
266
|
+
ok: errors.length === 0,
|
|
267
|
+
errors,
|
|
268
|
+
warnings,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
writeJsonFile(allowlistPath, updated);
|
|
272
|
+
}
|
|
273
|
+
if (args.check) {
|
|
274
|
+
if (!report.ok) {
|
|
275
|
+
console.error(JSON.stringify(report, null, 2));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
// Print a small success line for CI.
|
|
279
|
+
console.log(JSON.stringify(report, null, 2));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
console.log(JSON.stringify(report, null, 2));
|
|
283
|
+
}
|
|
284
|
+
main().catch((err) => {
|
|
285
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
function main() {
|
|
3
|
+
const raw = execSync("npm pack --dry-run --json", {
|
|
4
|
+
encoding: "utf-8",
|
|
5
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
6
|
+
});
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
9
|
+
throw new Error("Unable to parse npm pack --dry-run --json output.");
|
|
10
|
+
}
|
|
11
|
+
const pack = parsed[0];
|
|
12
|
+
const filePaths = new Set(pack.files.map((f) => f.path));
|
|
13
|
+
const required = ["dist/index.js", "config/dev-allowlist.json", "README.md", "package.json"];
|
|
14
|
+
const missing = required.filter((f) => !filePaths.has(f));
|
|
15
|
+
if (missing.length > 0) {
|
|
16
|
+
throw new Error(`Pack audit failed: required files missing: ${missing.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
const bannedPatterns = [
|
|
19
|
+
/^src\//,
|
|
20
|
+
/^test\//,
|
|
21
|
+
/^scripts\//,
|
|
22
|
+
/^test-output/,
|
|
23
|
+
/^diff\.txt$/,
|
|
24
|
+
/^tsconfig(?:\.test)?\.json$/,
|
|
25
|
+
/^package-lock\.json$/,
|
|
26
|
+
/^node_modules\//,
|
|
27
|
+
];
|
|
28
|
+
const banned = pack.files
|
|
29
|
+
.map((f) => f.path)
|
|
30
|
+
.filter((p) => bannedPatterns.some((re) => re.test(p)));
|
|
31
|
+
if (banned.length > 0) {
|
|
32
|
+
throw new Error(`Pack audit failed: unexpected files in tarball: ${banned.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
const distFiles = pack.files.filter((f) => f.path.startsWith("dist/"));
|
|
35
|
+
if (distFiles.length === 0) {
|
|
36
|
+
throw new Error("Pack audit failed: no dist/* files present in tarball.");
|
|
37
|
+
}
|
|
38
|
+
console.log(JSON.stringify({
|
|
39
|
+
ok: true,
|
|
40
|
+
filename: pack.filename,
|
|
41
|
+
file_count: pack.files.length,
|
|
42
|
+
dist_file_count: distFiles.length,
|
|
43
|
+
}, null, 2));
|
|
44
|
+
}
|
|
45
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { LLM_V2_ASPECTS_SCHEMA, LLM_V2_DICT, LLM_V2_POINTS_SCHEMA, LlmV2PayloadSchema, llmV2JsonSchema, } from "../src/schema-packs/llm.js";
|
|
6
|
+
function parseMode(argv) {
|
|
7
|
+
if (argv.includes("--write"))
|
|
8
|
+
return "write";
|
|
9
|
+
if (argv.includes("--check"))
|
|
10
|
+
return "check";
|
|
11
|
+
throw new Error("Usage: tsx scripts/schema-packs.ts --write|--check");
|
|
12
|
+
}
|
|
13
|
+
function ensureDir(dirPath) {
|
|
14
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
function readJsonFile(filePath) {
|
|
17
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
function writeJsonFile(filePath, obj) {
|
|
21
|
+
const raw = JSON.stringify(obj, null, 2) + "\n";
|
|
22
|
+
fs.writeFileSync(filePath, raw, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
function stableStringify(obj) {
|
|
25
|
+
// Deterministic enough for our use: we always write with JSON.stringify(..., null, 2)
|
|
26
|
+
// and compare against the same formatting.
|
|
27
|
+
return JSON.stringify(obj, null, 2) + "\n";
|
|
28
|
+
}
|
|
29
|
+
function validateFixture(filePath) {
|
|
30
|
+
const payload = readJsonFile(filePath);
|
|
31
|
+
const parsed = LlmV2PayloadSchema.safeParse(payload);
|
|
32
|
+
if (!parsed.success) {
|
|
33
|
+
const msg = parsed.error.issues
|
|
34
|
+
.slice(0, 25)
|
|
35
|
+
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
36
|
+
.join("\n");
|
|
37
|
+
throw new Error(`Invalid llm fixture: ${filePath}\n${msg}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const MCP_ROOT = path.resolve(SCRIPT_DIR, "..");
|
|
42
|
+
const PACK_DIR = path.join(MCP_ROOT, "schema-packs", "llm");
|
|
43
|
+
const FIXTURES_DIR = path.join(PACK_DIR, "fixtures");
|
|
44
|
+
const SCHEMA_PATH = path.join(PACK_DIR, "schema.json");
|
|
45
|
+
const REPO_ROOT = path.resolve(MCP_ROOT, "..");
|
|
46
|
+
const CANDIDATE_FIXTURES = [
|
|
47
|
+
path.join(REPO_ROOT, "tmp", "llm_fe_sample.json"),
|
|
48
|
+
path.join(REPO_ROOT, "tmp", "llm_payload_user_2025-12-21.json"),
|
|
49
|
+
];
|
|
50
|
+
function copyFirstExistingFixture() {
|
|
51
|
+
for (const candidate of CANDIDATE_FIXTURES) {
|
|
52
|
+
if (!fs.existsSync(candidate))
|
|
53
|
+
continue;
|
|
54
|
+
const dest = path.join(FIXTURES_DIR, path.basename(candidate));
|
|
55
|
+
fs.copyFileSync(candidate, dest);
|
|
56
|
+
return dest;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function writeFallbackFixture() {
|
|
61
|
+
const fallbackPath = path.join(FIXTURES_DIR, "minimal-natal.json");
|
|
62
|
+
const minimalPayload = {
|
|
63
|
+
output_mode: "llm",
|
|
64
|
+
chart: {
|
|
65
|
+
chart_type: "natal",
|
|
66
|
+
subject: {
|
|
67
|
+
datetime: "2026-01-01T00:00:00Z",
|
|
68
|
+
jd: null,
|
|
69
|
+
lat: 0,
|
|
70
|
+
lon: 0,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
dict: LLM_V2_DICT,
|
|
74
|
+
present: {
|
|
75
|
+
point_count: 1,
|
|
76
|
+
kinds: {
|
|
77
|
+
planet: 1,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
points_schema: LLM_V2_POINTS_SCHEMA,
|
|
81
|
+
points: [["sun", "planet", 0, 280, 0, 1, 0, 9, 10, null, 23, 0, "none", null, null, 0]],
|
|
82
|
+
aspects_schema: LLM_V2_ASPECTS_SCHEMA,
|
|
83
|
+
aspects: [],
|
|
84
|
+
counts: {
|
|
85
|
+
point_count: 1,
|
|
86
|
+
aspect_count: 0,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
writeJsonFile(fallbackPath, minimalPayload);
|
|
90
|
+
return fallbackPath;
|
|
91
|
+
}
|
|
92
|
+
function listFixtures() {
|
|
93
|
+
if (!fs.existsSync(FIXTURES_DIR))
|
|
94
|
+
return [];
|
|
95
|
+
return fs
|
|
96
|
+
.readdirSync(FIXTURES_DIR)
|
|
97
|
+
.filter((f) => f.toLowerCase().endsWith(".json"))
|
|
98
|
+
.map((f) => path.join(FIXTURES_DIR, f));
|
|
99
|
+
}
|
|
100
|
+
function checkSchemaFile() {
|
|
101
|
+
if (!fs.existsSync(SCHEMA_PATH)) {
|
|
102
|
+
throw new Error(`Missing schema file: ${SCHEMA_PATH} (run --write)`);
|
|
103
|
+
}
|
|
104
|
+
const disk = readJsonFile(SCHEMA_PATH);
|
|
105
|
+
const expected = llmV2JsonSchema;
|
|
106
|
+
const diskRaw = fs.readFileSync(SCHEMA_PATH, "utf-8");
|
|
107
|
+
const expectedRaw = stableStringify(expected);
|
|
108
|
+
if (diskRaw !== expectedRaw) {
|
|
109
|
+
throw new Error(`schema.json drift detected (run --write): ${SCHEMA_PATH}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function main() {
|
|
113
|
+
const mode = parseMode(process.argv.slice(2));
|
|
114
|
+
if (mode === "write") {
|
|
115
|
+
ensureDir(FIXTURES_DIR);
|
|
116
|
+
ensureDir(PACK_DIR);
|
|
117
|
+
writeJsonFile(SCHEMA_PATH, llmV2JsonSchema);
|
|
118
|
+
if (listFixtures().length === 0) {
|
|
119
|
+
const copied = copyFirstExistingFixture();
|
|
120
|
+
if (copied) {
|
|
121
|
+
validateFixture(copied);
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.log(`Copied fixture: ${copied}`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const fallback = writeFallbackFixture();
|
|
127
|
+
validateFixture(fallback);
|
|
128
|
+
// eslint-disable-next-line no-console
|
|
129
|
+
console.log(`Wrote fallback fixture: ${fallback}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const fixture of listFixtures()) {
|
|
133
|
+
validateFixture(fixture);
|
|
134
|
+
}
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log(`Wrote schema pack: ${PACK_DIR}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// check
|
|
140
|
+
checkSchemaFile();
|
|
141
|
+
const fixtures = listFixtures();
|
|
142
|
+
if (fixtures.length === 0) {
|
|
143
|
+
throw new Error(`No fixtures found in: ${FIXTURES_DIR}`);
|
|
144
|
+
}
|
|
145
|
+
for (const fixture of fixtures)
|
|
146
|
+
validateFixture(fixture);
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.log(`Schema pack OK (fixtures=${fixtures.length})`);
|
|
149
|
+
}
|
|
150
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { initTools, toolRegistry } from "../src/tools/index.js";
|
|
2
|
+
async function main() {
|
|
3
|
+
await initTools("dev");
|
|
4
|
+
const list = toolRegistry["dev.list_allowed"];
|
|
5
|
+
const call = toolRegistry["dev.call"];
|
|
6
|
+
if (!list || !call) {
|
|
7
|
+
throw new Error("Dev tools not registered (expected dev.list_allowed and dev.call)");
|
|
8
|
+
}
|
|
9
|
+
const allowed = await list.handler({});
|
|
10
|
+
console.log("ALLOWED_TOOLS:");
|
|
11
|
+
console.log(JSON.stringify(Object.keys(toolRegistry), null, 2));
|
|
12
|
+
console.log("\nALLOWLIST:");
|
|
13
|
+
console.log(JSON.stringify(allowed, null, 2));
|
|
14
|
+
// Smoke call a simple endpoint that should exist without extra payload.
|
|
15
|
+
const result = await call.handler({
|
|
16
|
+
method: "GET",
|
|
17
|
+
path: "/health",
|
|
18
|
+
});
|
|
19
|
+
console.log("\nDEV_CALL_RESULT:");
|
|
20
|
+
console.log(JSON.stringify(result, null, 2));
|
|
21
|
+
}
|
|
22
|
+
main().catch((err) => {
|
|
23
|
+
console.error(err instanceof Error ? err.stack || err.message : String(err));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { initTools, toolRegistry } from "../src/tools/index.js";
|
|
5
|
+
const CURSOR_BEGIN = "<!-- GENERATED:CURSOR_INSTALL:BEGIN -->";
|
|
6
|
+
const CURSOR_END = "<!-- GENERATED:CURSOR_INSTALL:END -->";
|
|
7
|
+
const SNAPSHOT_BEGIN = "<!-- GENERATED:RUNTIME_SNAPSHOT:BEGIN -->";
|
|
8
|
+
const SNAPSHOT_END = "<!-- GENERATED:RUNTIME_SNAPSHOT:END -->";
|
|
9
|
+
function parseMode(argv) {
|
|
10
|
+
if (argv.includes("--write"))
|
|
11
|
+
return "write";
|
|
12
|
+
if (argv.includes("--check"))
|
|
13
|
+
return "check";
|
|
14
|
+
throw new Error("Usage: tsx scripts/sync-readme.ts --write|--check");
|
|
15
|
+
}
|
|
16
|
+
function replaceSection(content, beginMarker, endMarker, body) {
|
|
17
|
+
const start = content.indexOf(beginMarker);
|
|
18
|
+
const end = content.indexOf(endMarker);
|
|
19
|
+
if (start === -1 || end === -1 || end < start) {
|
|
20
|
+
throw new Error(`Missing marker block: ${beginMarker} ... ${endMarker}`);
|
|
21
|
+
}
|
|
22
|
+
const before = content.slice(0, start + beginMarker.length);
|
|
23
|
+
const after = content.slice(end);
|
|
24
|
+
return `${before}\n${body.trim()}\n${after}`;
|
|
25
|
+
}
|
|
26
|
+
function getRepoPaths() {
|
|
27
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const mcpRoot = path.resolve(scriptDir, "..");
|
|
29
|
+
return {
|
|
30
|
+
readmePath: path.join(mcpRoot, "README.md"),
|
|
31
|
+
allowlistPath: path.join(mcpRoot, "config", "dev-allowlist.json"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function loadAllowlist(allowlistPath) {
|
|
35
|
+
const raw = fs.readFileSync(allowlistPath, "utf-8");
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (parsed.schema !== "astromcp-dev-allowlist-v1" || !Array.isArray(parsed.allow)) {
|
|
38
|
+
throw new Error(`Invalid allowlist at ${allowlistPath}`);
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
function toCursorDeepLink() {
|
|
43
|
+
const config = {
|
|
44
|
+
command: "npx",
|
|
45
|
+
args: ["-y", "@openephemeris/mcp-server"],
|
|
46
|
+
env: {
|
|
47
|
+
OPENEPHEMERIS_PROFILE: "dev",
|
|
48
|
+
OPENEPHEMERIS_BACKEND_URL: "https://api.openephemeris.com",
|
|
49
|
+
OPENEPHEMERIS_API_KEY: "YOUR_API_KEY_HERE",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
const configJson = JSON.stringify(config, null, 2);
|
|
53
|
+
const encoded = Buffer.from(JSON.stringify(config), "utf-8").toString("base64");
|
|
54
|
+
const url = `cursor://anysphere.cursor-deeplink/mcp/install?name=openephemeris&config=${encoded}`;
|
|
55
|
+
return { url, configJson };
|
|
56
|
+
}
|
|
57
|
+
function buildMethodCounts(entries) {
|
|
58
|
+
const counts = { GET: 0, POST: 0, PUT: 0, PATCH: 0, DELETE: 0 };
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
counts[e.method] += 1;
|
|
61
|
+
}
|
|
62
|
+
return counts;
|
|
63
|
+
}
|
|
64
|
+
function buildFamilyRows(entries) {
|
|
65
|
+
const families = new Map();
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
const family = e.path.split("/").filter(Boolean)[0] || "root";
|
|
68
|
+
const existing = families.get(family) || [];
|
|
69
|
+
existing.push(e);
|
|
70
|
+
families.set(family, existing);
|
|
71
|
+
}
|
|
72
|
+
const rows = [...families.entries()]
|
|
73
|
+
.map(([family, items]) => {
|
|
74
|
+
const sorted = [...items].sort((a, b) => `${a.path}:${a.method}`.localeCompare(`${b.path}:${b.method}`));
|
|
75
|
+
const sample = sorted.slice(0, 2).map((e) => `\`${e.method} ${e.path}\``).join(", ");
|
|
76
|
+
return { family, count: items.length, sample };
|
|
77
|
+
})
|
|
78
|
+
.sort((a, b) => a.family.localeCompare(b.family));
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
async function buildRuntimeSnapshot(allow) {
|
|
82
|
+
await initTools("dev");
|
|
83
|
+
const toolNames = Object.keys(toolRegistry).sort((a, b) => a.localeCompare(b));
|
|
84
|
+
const typedTools = toolNames.filter((t) => !t.startsWith("dev."));
|
|
85
|
+
const genericTools = toolNames.filter((t) => t.startsWith("dev."));
|
|
86
|
+
const methodCounts = buildMethodCounts(allow);
|
|
87
|
+
const familyRows = buildFamilyRows(allow);
|
|
88
|
+
const familiesTable = [
|
|
89
|
+
"| Family | Operations | Example |",
|
|
90
|
+
"|---|---:|---|",
|
|
91
|
+
...familyRows.map((row) => `| \`${row.family}\` | ${row.count} | ${row.sample} |`),
|
|
92
|
+
].join("\n");
|
|
93
|
+
return [
|
|
94
|
+
"## Runtime Snapshot (Generated)",
|
|
95
|
+
"",
|
|
96
|
+
"Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live tool registry.",
|
|
97
|
+
"",
|
|
98
|
+
`- Allowlisted operations: **${allow.length}**`,
|
|
99
|
+
`- Methods: \`GET=${methodCounts.GET}\`, \`POST=${methodCounts.POST}\`, \`PUT=${methodCounts.PUT}\`, \`PATCH=${methodCounts.PATCH}\`, \`DELETE=${methodCounts.DELETE}\``,
|
|
100
|
+
`- Registered tools (\`OPENEPHEMERIS_PROFILE=dev\`): **${toolNames.length}**`,
|
|
101
|
+
`- Typed tools: ${typedTools.map((t) => `\`${t}\``).join(", ")}`,
|
|
102
|
+
`- Generic tools: ${genericTools.map((t) => `\`${t}\``).join(", ")}`,
|
|
103
|
+
"",
|
|
104
|
+
"### Allowlist Families",
|
|
105
|
+
"",
|
|
106
|
+
familiesTable,
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
async function main() {
|
|
110
|
+
const mode = parseMode(process.argv.slice(2));
|
|
111
|
+
const { readmePath, allowlistPath } = getRepoPaths();
|
|
112
|
+
const allowlist = loadAllowlist(allowlistPath);
|
|
113
|
+
const { url, configJson } = toCursorDeepLink();
|
|
114
|
+
const runtimeSnapshot = await buildRuntimeSnapshot(allowlist.allow);
|
|
115
|
+
const cursorSection = [
|
|
116
|
+
`[](${url})`,
|
|
117
|
+
"",
|
|
118
|
+
"> Replace `YOUR_API_KEY_HERE` in Cursor MCP settings with your API key from https://openephemeris.com/dashboard.",
|
|
119
|
+
"",
|
|
120
|
+
"Cursor deeplink payload:",
|
|
121
|
+
"```json",
|
|
122
|
+
configJson,
|
|
123
|
+
"```",
|
|
124
|
+
].join("\n");
|
|
125
|
+
const original = fs.readFileSync(readmePath, "utf-8");
|
|
126
|
+
let next = replaceSection(original, CURSOR_BEGIN, CURSOR_END, cursorSection);
|
|
127
|
+
next = replaceSection(next, SNAPSHOT_BEGIN, SNAPSHOT_END, runtimeSnapshot);
|
|
128
|
+
if (mode === "check") {
|
|
129
|
+
if (next !== original) {
|
|
130
|
+
throw new Error("README drift detected. Run `npm run sync:readme`.");
|
|
131
|
+
}
|
|
132
|
+
console.log("README sync OK");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
fs.writeFileSync(readmePath, next, "utf-8");
|
|
136
|
+
console.log("README updated");
|
|
137
|
+
}
|
|
138
|
+
main().catch((err) => {
|
|
139
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|