@inbeat/standards 0.7.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/CHANGELOG.md +100 -0
- package/agents/app-section.md +22 -0
- package/agents/standard.md +114 -0
- package/package.json +46 -0
- package/skills/ib-feature/SKILL.md +40 -0
- package/skills/ib-toolkit/SKILL.md +125 -0
- package/skills/ib-ui/SKILL.md +155 -0
- package/src/cli.ts +85 -0
- package/src/create-args.ts +10 -0
- package/src/create.ts +240 -0
- package/src/foundation.ts +42 -0
- package/src/mcp-servers.ts +8 -0
- package/src/mcp.ts +383 -0
- package/src/sync.ts +117 -0
- package/src/template.ts +34 -0
- package/template/.agents/skills/ib-feature/SKILL.md +40 -0
- package/template/.agents/skills/ib-toolkit/SKILL.md +125 -0
- package/template/.agents/skills/ib-ui/SKILL.md +155 -0
- package/template/.dependency-cruiser.cjs +1 -0
- package/template/.env.example +12 -0
- package/template/.github/workflows/ci.yml +14 -0
- package/template/AGENTS.md +139 -0
- package/template/_gitignore +10 -0
- package/template/biome.json +6 -0
- package/template/next.config.ts +32 -0
- package/template/package.json +62 -0
- package/template/postcss.config.mjs +7 -0
- package/template/sentry.edge.config.ts +11 -0
- package/template/sentry.server.config.ts +12 -0
- package/template/src/app/api/health/route.test.ts +12 -0
- package/template/src/app/api/health/route.ts +7 -0
- package/template/src/app/error.tsx +24 -0
- package/template/src/app/global-error.tsx +20 -0
- package/template/src/app/globals.css +4 -0
- package/template/src/app/icon.svg +1 -0
- package/template/src/app/layout.tsx +24 -0
- package/template/src/app/loading.tsx +9 -0
- package/template/src/app/not-found.tsx +14 -0
- package/template/src/app/page.tsx +7 -0
- package/template/src/app/robots.ts +6 -0
- package/template/src/instrumentation-client.ts +14 -0
- package/template/src/instrumentation.ts +13 -0
- package/template/src/shared/config/env.ts +19 -0
- package/template/src/shared/config/routes.ts +3 -0
- package/template/src/test/server-only.ts +1 -0
- package/template/tsconfig.json +10 -0
- package/template/vitest.config.ts +14 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { create } from "./create";
|
|
4
|
+
import { parseCreateArgs } from "./create-args";
|
|
5
|
+
import { foundationProblems } from "./foundation";
|
|
6
|
+
import { runMcp } from "./mcp";
|
|
7
|
+
import { claudeFilesIgnoringAgents, staleFiles, write } from "./sync";
|
|
8
|
+
|
|
9
|
+
const USAGE = [
|
|
10
|
+
"usage: ib <create <dir> [--local] | sync [app-dir] | check [app-dir] | mcp>",
|
|
11
|
+
" create <dir> start a new app",
|
|
12
|
+
" mcp write MCP server config for your AI tools; run `ib mcp` for the picker",
|
|
13
|
+
" --local write absolute file: paths to ib-toolkit tarballs; for toolkit development and CI only,",
|
|
14
|
+
" never commit that package.json",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
18
|
+
const appDir = resolve(args.find((arg) => !arg.startsWith("--")) ?? ".");
|
|
19
|
+
|
|
20
|
+
const commands: Record<string, () => number | Promise<number>> = {
|
|
21
|
+
create: async () => {
|
|
22
|
+
let parsed: ReturnType<typeof parseCreateArgs>;
|
|
23
|
+
try {
|
|
24
|
+
parsed = parseCreateArgs(args);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`${error instanceof Error ? error.message : error}\n${USAGE}`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
if (!parsed.dir) {
|
|
30
|
+
console.error(USAGE);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
await create(parsed.dir, { local: parsed.local });
|
|
34
|
+
return 0;
|
|
35
|
+
},
|
|
36
|
+
sync: () => {
|
|
37
|
+
const written = write(appDir);
|
|
38
|
+
console.info(written.length ? `updated:\n ${written.join("\n ")}` : "already up to date");
|
|
39
|
+
return 0;
|
|
40
|
+
},
|
|
41
|
+
check: () => {
|
|
42
|
+
const stale = staleFiles(appDir);
|
|
43
|
+
const ignoring = claudeFilesIgnoringAgents(appDir);
|
|
44
|
+
const problems = foundationProblems(appDir);
|
|
45
|
+
if (stale.length > 0) {
|
|
46
|
+
console.error(
|
|
47
|
+
`out of date with @inbeat/standards, run \`ib sync\`:\n ${stale.join("\n ")}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (ignoring.length > 0) {
|
|
51
|
+
console.error(
|
|
52
|
+
"Claude Code ignores AGENTS.md when a CLAUDE.md exists; add a line `@AGENTS.md` to:\n" +
|
|
53
|
+
` ${ignoring.join("\n ")}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (problems.length > 0) {
|
|
57
|
+
console.error(`not on the shared foundation:\n ${problems.join("\n ")}`);
|
|
58
|
+
}
|
|
59
|
+
return stale.length + ignoring.length + problems.length === 0 ? 0 : 1;
|
|
60
|
+
},
|
|
61
|
+
mcp: () =>
|
|
62
|
+
runMcp(args, {
|
|
63
|
+
cwd: process.cwd(),
|
|
64
|
+
stdin: process.stdin,
|
|
65
|
+
output: process.stdout,
|
|
66
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
67
|
+
out: console.info,
|
|
68
|
+
err: console.error,
|
|
69
|
+
}),
|
|
70
|
+
help: () => {
|
|
71
|
+
console.info(USAGE);
|
|
72
|
+
return 0;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const wantsHelp = args.some((arg) => arg === "--help" || arg === "-h");
|
|
77
|
+
const handler = wantsHelp && command !== "mcp" ? commands.help : commands[command];
|
|
78
|
+
if (!handler) console.error(USAGE);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
process.exit(handler ? await handler() : 1);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(error instanceof Error ? error.message : error);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CreateArgs = { dir: string | undefined; local: boolean };
|
|
2
|
+
|
|
3
|
+
export function parseCreateArgs(args: string[]): CreateArgs {
|
|
4
|
+
const flags = args.filter((arg) => arg.startsWith("--"));
|
|
5
|
+
const unknown = flags.filter((flag) => flag !== "--local");
|
|
6
|
+
if (unknown.length > 0) throw new Error(`unknown option ${unknown.join(", ")}`);
|
|
7
|
+
const positional = args.filter((arg) => !arg.startsWith("--"));
|
|
8
|
+
if (positional.length > 1) throw new Error("create takes one directory");
|
|
9
|
+
return { dir: positional[0], local: flags.includes("--local") };
|
|
10
|
+
}
|
package/src/create.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
12
|
+
import { PACKAGE_ROOT, VERSION, write } from "./sync";
|
|
13
|
+
import { isShipped, restoreDotfileName, templateEntries } from "./template";
|
|
14
|
+
|
|
15
|
+
const DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "peerDependencies"] as const;
|
|
16
|
+
|
|
17
|
+
type Dependencies = Record<string, string>;
|
|
18
|
+
export type Manifest = {
|
|
19
|
+
name: string;
|
|
20
|
+
overrides?: Dependencies;
|
|
21
|
+
dependencies?: Dependencies;
|
|
22
|
+
devDependencies?: Dependencies;
|
|
23
|
+
peerDependencies?: Dependencies;
|
|
24
|
+
[field: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
|
|
28
|
+
|
|
29
|
+
export function projectName(dir: string): string {
|
|
30
|
+
return basename(resolve(dir))
|
|
31
|
+
.replace(/([a-z\d])([A-Z])/g, "$1-$2")
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z\d]+/g, "-")
|
|
34
|
+
.replace(/^-+|-+$/g, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validateProjectName(name: string): void {
|
|
38
|
+
const isValid =
|
|
39
|
+
name.length > 0 &&
|
|
40
|
+
name.length <= 214 &&
|
|
41
|
+
/^[a-z0-9][a-z0-9-]*$/.test(name) &&
|
|
42
|
+
!["node_modules", "favicon.ico"].includes(name);
|
|
43
|
+
if (!isValid) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`"${name}" cannot be an npm package name; use a folder name of lowercase letters, digits and dashes, e.g. my-app.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function findConflicts(targetDir: string, templateEntries: string[]): string[] {
|
|
51
|
+
return templateEntries
|
|
52
|
+
.map(restoreDotfileName)
|
|
53
|
+
.filter((entry) => existsSync(join(targetDir, entry)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function rewriteManifest(
|
|
57
|
+
manifest: Manifest,
|
|
58
|
+
options: { name: string; version: string; tarballs?: Dependencies },
|
|
59
|
+
): Manifest {
|
|
60
|
+
const { name, version, tarballs } = options;
|
|
61
|
+
const resolveRange = (dependency: string, range: string): string => {
|
|
62
|
+
if (!range.startsWith("workspace:")) return range;
|
|
63
|
+
return tarballs?.[dependency] ?? `^${version}`;
|
|
64
|
+
};
|
|
65
|
+
const resolved = Object.fromEntries(
|
|
66
|
+
DEPENDENCY_FIELDS.filter((field) => manifest[field]).map((field) => [
|
|
67
|
+
field,
|
|
68
|
+
Object.fromEntries(
|
|
69
|
+
Object.entries(manifest[field] as Dependencies).map(([dependency, range]) => [
|
|
70
|
+
dependency,
|
|
71
|
+
resolveRange(dependency, range),
|
|
72
|
+
]),
|
|
73
|
+
),
|
|
74
|
+
]),
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
...manifest,
|
|
78
|
+
...resolved,
|
|
79
|
+
name,
|
|
80
|
+
...(tarballs && { overrides: tarballs }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function findToolkitRoot(from: string = PACKAGE_ROOT): string | null {
|
|
85
|
+
const manifestPath = join(from, "package.json");
|
|
86
|
+
const isToolkit =
|
|
87
|
+
existsSync(join(from, "scripts", "pack-local.ts")) &&
|
|
88
|
+
existsSync(manifestPath) &&
|
|
89
|
+
JSON.parse(readFileSync(manifestPath, "utf8")).name === "ib-toolkit";
|
|
90
|
+
if (isToolkit) return from;
|
|
91
|
+
const parent = dirname(from);
|
|
92
|
+
return parent === from ? null : findToolkitRoot(parent);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type Runner = (command: string, args: string[], cwd: string) => void;
|
|
96
|
+
|
|
97
|
+
type ToolkitScripts = {
|
|
98
|
+
toolkitPackages(): { name: string; shortName: string }[];
|
|
99
|
+
packTarballPath(pkg: { shortName: string }): string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Loaded from the checkout at runtime: scripts/ only exists there, never in the published package.
|
|
103
|
+
async function localTarballs(toolkitRoot: string): Promise<Dependencies> {
|
|
104
|
+
const scripts: ToolkitScripts = await import(join(toolkitRoot, "scripts", "packages.ts"));
|
|
105
|
+
return Object.fromEntries(
|
|
106
|
+
scripts.toolkitPackages().map((pkg) => [pkg.name, `file:${scripts.packTarballPath(pkg)}`]),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const runCommand: Runner = (command, args, cwd) => {
|
|
111
|
+
const { status, error } = spawnSync(command, args, { cwd, stdio: "inherit" });
|
|
112
|
+
if (status !== 0) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`\`${[command, ...args].join(" ")}\` failed${error ? `: ${error.message}` : ""}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function isInsideGitRepo(dir: string): boolean {
|
|
120
|
+
return (
|
|
121
|
+
spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "ignore" })
|
|
122
|
+
.status === 0
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveToolkitRoot(local: boolean, targetDir: string): string | null {
|
|
127
|
+
if (findToolkitRoot(targetDir)) {
|
|
128
|
+
throw new Error(`${targetDir} is inside the ib-toolkit checkout; create projects elsewhere.`);
|
|
129
|
+
}
|
|
130
|
+
if (local) {
|
|
131
|
+
const toolkitRoot = findToolkitRoot();
|
|
132
|
+
if (!toolkitRoot) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"--local only works from an ib-toolkit checkout (run the checkout's src/cli.ts).",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return toolkitRoot;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type CreateOptions = {
|
|
143
|
+
local: boolean;
|
|
144
|
+
templateDir?: string;
|
|
145
|
+
run?: Runner;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function populate(targetDir: string, templateDir: string, manifest: Manifest, run: Runner): void {
|
|
149
|
+
for (const entry of templateEntries(templateDir)) {
|
|
150
|
+
cpSync(join(templateDir, entry), join(targetDir, restoreDotfileName(entry)), {
|
|
151
|
+
recursive: true,
|
|
152
|
+
filter: (path) => isShipped(relative(templateDir, path)),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const manifestPath = join(targetDir, "package.json");
|
|
156
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
157
|
+
|
|
158
|
+
const envLocalPath = join(targetDir, ".env.local");
|
|
159
|
+
if (!existsSync(envLocalPath)) cpSync(join(targetDir, ".env.example"), envLocalPath);
|
|
160
|
+
|
|
161
|
+
if (!isInsideGitRepo(targetDir)) run("git", ["init"], targetDir);
|
|
162
|
+
write(targetDir);
|
|
163
|
+
run("bun", ["install"], targetDir);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function removeWrittenPaths(targetDir: string, preexisting: Set<string> | null): string {
|
|
167
|
+
if (!preexisting) {
|
|
168
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
169
|
+
return `removed ${targetDir}`;
|
|
170
|
+
}
|
|
171
|
+
const written = readdirSync(targetDir).filter((entry) => !preexisting.has(entry));
|
|
172
|
+
for (const entry of written) rmSync(join(targetDir, entry), { recursive: true, force: true });
|
|
173
|
+
return `removed from ${targetDir}: ${written.join(", ")}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function assertNoConflicts(targetDir: string, plannedEntries: string[]): void {
|
|
177
|
+
const conflicts = findConflicts(targetDir, plannedEntries);
|
|
178
|
+
if (conflicts.length > 0) {
|
|
179
|
+
throw new Error(`${targetDir} already contains template paths:\n ${conflicts.join("\n ")}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function locationStep(cwd: string, targetDir: string): string | null {
|
|
184
|
+
const location = relative(cwd, targetDir);
|
|
185
|
+
if (!location) return null;
|
|
186
|
+
return `cd ${location.startsWith("..") ? targetDir : location}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function formatNextSteps(steps: string[]): string {
|
|
190
|
+
return ["Next steps:", ...[...steps, "bun dev"].map((step) => ` ${step}`)].join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Everything that can be rejected before the template is built or anything is written. */
|
|
194
|
+
function preflight(
|
|
195
|
+
dir: string,
|
|
196
|
+
options: Pick<CreateOptions, "local" | "templateDir">,
|
|
197
|
+
): { targetDir: string; name: string; toolkitRoot: string | null } {
|
|
198
|
+
const { local, templateDir = TEMPLATE_DIR } = options;
|
|
199
|
+
const targetDir = resolve(dir);
|
|
200
|
+
const name = projectName(targetDir);
|
|
201
|
+
validateProjectName(name);
|
|
202
|
+
const toolkitRoot = resolveToolkitRoot(local, targetDir);
|
|
203
|
+
if (existsSync(templateDir)) assertNoConflicts(targetDir, templateEntries(templateDir));
|
|
204
|
+
return { targetDir, name, toolkitRoot };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function create(dir: string, options: CreateOptions): Promise<void> {
|
|
208
|
+
const { templateDir = TEMPLATE_DIR, run = runCommand } = options;
|
|
209
|
+
const { targetDir, name, toolkitRoot } = preflight(dir, options);
|
|
210
|
+
if (toolkitRoot) run("bun", ["run", "pack:local"], toolkitRoot);
|
|
211
|
+
|
|
212
|
+
if (!existsSync(templateDir)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"template missing from @inbeat/standards; run `bun run build:packages` in ib-toolkit.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
assertNoConflicts(targetDir, templateEntries(templateDir));
|
|
218
|
+
|
|
219
|
+
const manifest = rewriteManifest(
|
|
220
|
+
JSON.parse(readFileSync(join(templateDir, "package.json"), "utf8")),
|
|
221
|
+
{
|
|
222
|
+
name,
|
|
223
|
+
version: VERSION,
|
|
224
|
+
tarballs: toolkitRoot ? await localTarballs(toolkitRoot) : undefined,
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
const preexisting = existsSync(targetDir) ? new Set(readdirSync(targetDir)) : null;
|
|
228
|
+
try {
|
|
229
|
+
populate(targetDir, templateDir, manifest, run);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const cleanup = removeWrittenPaths(targetDir, preexisting);
|
|
232
|
+
throw new Error(`create failed: ${error instanceof Error ? error.message : error}\n${cleanup}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const location = locationStep(process.cwd(), targetDir);
|
|
236
|
+
const steps = location ? [location] : [];
|
|
237
|
+
console.info(
|
|
238
|
+
`\nSuccess! Created ${name} at ${realpathSync(targetDir)}\n\n${formatNextSteps(steps)}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
type Requirement = {
|
|
5
|
+
files: string[];
|
|
6
|
+
message: string;
|
|
7
|
+
satisfiedBy: (contents: string) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// biome.jsonc and tsconfig.json are JSONC (comments, trailing commas), so match the quoted preset
|
|
11
|
+
// name instead of parsing.
|
|
12
|
+
const mentionsPreset = (preset: string) => (contents: string) => contents.includes(`"${preset}"`);
|
|
13
|
+
|
|
14
|
+
const REQUIREMENTS: Requirement[] = [
|
|
15
|
+
{
|
|
16
|
+
files: ["biome.json", "biome.jsonc"],
|
|
17
|
+
message: "biome.json (or biome.jsonc) must extend @inbeat/config/biome",
|
|
18
|
+
satisfiedBy: mentionsPreset("@inbeat/config/biome"),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
files: ["tsconfig.json"],
|
|
22
|
+
message: "tsconfig.json must extend @inbeat/config/tsconfig/nextjs.json",
|
|
23
|
+
satisfiedBy: mentionsPreset("@inbeat/config/tsconfig/nextjs.json"),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
files: [".dependency-cruiser.cjs"],
|
|
27
|
+
message: ".dependency-cruiser.cjs must use @inbeat/config/dependency-cruiser",
|
|
28
|
+
satisfiedBy: mentionsPreset("@inbeat/config/dependency-cruiser"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
files: ["vitest.config.ts"],
|
|
32
|
+
message: "vitest.config.ts must import from @inbeat/config/vitest",
|
|
33
|
+
satisfiedBy: mentionsPreset("@inbeat/config/vitest"),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function foundationProblems(appDir: string): string[] {
|
|
38
|
+
return REQUIREMENTS.filter(({ files, satisfiedBy }) => {
|
|
39
|
+
const path = files.map((file) => join(appDir, file)).find(existsSync);
|
|
40
|
+
return !path || !satisfiedBy(readFileSync(path, "utf8"));
|
|
41
|
+
}).map(({ message }) => message);
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const CURATED_SERVERS: Record<string, string> = {
|
|
2
|
+
vercel: "https://mcp.vercel.com",
|
|
3
|
+
github: "https://api.githubcopilot.com/mcp/readonly",
|
|
4
|
+
sentry: "https://mcp.sentry.dev/mcp",
|
|
5
|
+
neon: "https://mcp.neon.tech/mcp",
|
|
6
|
+
clerk: "https://mcp.clerk.com/mcp",
|
|
7
|
+
cloudflare: "https://mcp.cloudflare.com/mcp",
|
|
8
|
+
};
|