fix: live-run wiring (SDK 1.29, zod 3.25, OAUTH_ISSUER split, alt host ports, web on 3001, log level cast, pino transport)
- Bump @modelcontextprotocol/sdk from 1.0.4 to 1.29.0 in runner-template (1.0.4 has no McpServer or StreamableHTTPServerTransport — file not found at runtime). - Bump zod to 3.25.76 across workspace to satisfy modern SDK peer dep. - Split OAUTH_ISSUER (canonical, host-reachable) from CONTROL_PLANE_URL (container-reachable for JWKS). Runner verifies iss against OAUTH_ISSUER; fetches JWKS from CONTROL_PLANE_URL. Both API and runner now agree on http://localhost:4000/oauth as the issuer in dev. - Move postgres host port 5432 to 5440, redis 6379 to 6390 to avoid collisions with native installs on the dev machine. - Move web from 3000 to 3001 (3000 occupied by Gitea on dev machine). - Drop pino-pretty transport from API to avoid runtime require of an unbundled dep. - Cast build_logs.level (varchar) to BuildEvent's literal union in WS replay path. - Remove unused reqBase helper in oauth.ts.
This commit is contained in:
parent
ea1ec1e801
commit
ab67203921
@ -2,13 +2,13 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# ---- Database ----
|
# ---- Database ----
|
||||||
DATABASE_URL=postgresql://bmm:bmm@localhost:5432/bmm
|
DATABASE_URL=postgresql://bmm:bmm@localhost:5440/bmm
|
||||||
REDIS_URL=redis://localhost:6379
|
REDIS_URL=redis://localhost:6390
|
||||||
|
|
||||||
# ---- Auth (Better-Auth) ----
|
# ---- Auth (Better-Auth) ----
|
||||||
BETTER_AUTH_SECRET=replace-me-with-32-bytes-of-random-hex-1234567890abcdef
|
BETTER_AUTH_SECRET=replace-me-with-32-bytes-of-random-hex-1234567890abcdef
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3001
|
||||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
NEXT_PUBLIC_APP_URL=http://localhost:3001
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:4000
|
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||||
|
|
||||||
# ---- GitHub OAuth (optional in dev) ----
|
# ---- GitHub OAuth (optional in dev) ----
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"fastify": "5.2.0",
|
"fastify": "5.2.0",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"jose": "5.9.6",
|
"jose": "5.9.6",
|
||||||
"zod": "3.23.8"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.2",
|
||||||
|
|||||||
@ -5,13 +5,14 @@ const Env = z.object({
|
|||||||
DATABASE_URL: z.string(),
|
DATABASE_URL: z.string(),
|
||||||
REDIS_URL: z.string().default('redis://localhost:6379'),
|
REDIS_URL: z.string().default('redis://localhost:6379'),
|
||||||
PORT: z.coerce.number().default(4000),
|
PORT: z.coerce.number().default(4000),
|
||||||
NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3000'),
|
NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3001'),
|
||||||
OAUTH_KEY_DIR: z.string().default('./keys'),
|
OAUTH_KEY_DIR: z.string().default('./keys'),
|
||||||
ANTHROPIC_API_KEY: z.string().optional(),
|
ANTHROPIC_API_KEY: z.string().optional(),
|
||||||
SECRETS_ENCRYPTION_KEY: z
|
SECRETS_ENCRYPTION_KEY: z
|
||||||
.string()
|
.string()
|
||||||
.min(64, '32 bytes hex required')
|
.min(64, '32 bytes hex required')
|
||||||
.default('0000000000000000000000000000000000000000000000000000000000000000'),
|
.default('0000000000000000000000000000000000000000000000000000000000000000'),
|
||||||
|
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = Env.parse({
|
export const config = Env.parse({
|
||||||
|
|||||||
@ -10,10 +10,6 @@ import { oauthRoutes } from './routes/oauth.js';
|
|||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: {
|
logger: {
|
||||||
level: config.NODE_ENV === 'production' ? 'info' : 'debug',
|
level: config.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||||
transport:
|
|
||||||
config.NODE_ENV === 'development'
|
|
||||||
? { target: 'pino-pretty', options: { colorize: true, singleLine: true } }
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,7 @@ async function resolveServerByResource(resource: string) {
|
|||||||
export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
||||||
// Authorization Server Metadata (RFC 8414) — control-plane wide
|
// Authorization Server Metadata (RFC 8414) — control-plane wide
|
||||||
app.get('/oauth/.well-known/oauth-authorization-server', async (_req, reply) => {
|
app.get('/oauth/.well-known/oauth-authorization-server', async (_req, reply) => {
|
||||||
const base = `${reqBase(_req)}`;
|
const base = `${config.CONTROL_PLANE_PUBLIC_URL}`;
|
||||||
return reply.send({
|
return reply.send({
|
||||||
issuer: `${base}/oauth`,
|
issuer: `${base}/oauth`,
|
||||||
authorization_endpoint: `${base}/oauth/authorize`,
|
authorization_endpoint: `${base}/oauth/authorize`,
|
||||||
@ -215,7 +215,7 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
const accessToken = await signAccessToken({
|
const accessToken = await signAccessToken({
|
||||||
subject: row.code.userId ?? row.client.clientId,
|
subject: row.code.userId ?? row.client.clientId,
|
||||||
audience: resource,
|
audience: resource,
|
||||||
issuer: `${reqBase(req)}/oauth`,
|
issuer: `${config.CONTROL_PLANE_PUBLIC_URL}/oauth`,
|
||||||
scope: row.code.scope ?? '',
|
scope: row.code.scope ?? '',
|
||||||
ttlSeconds: 3600,
|
ttlSeconds: 3600,
|
||||||
});
|
});
|
||||||
@ -249,7 +249,7 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!parsed.success) return reply.code(400).send({ error: 'invalid_request' });
|
if (!parsed.success) return reply.code(400).send({ error: 'invalid_request' });
|
||||||
const server = await resolveServerByResource(parsed.data.resource);
|
const server = await resolveServerByResource(parsed.data.resource);
|
||||||
if (!server) return reply.code(404).send({ error: 'not_found' });
|
if (!server) return reply.code(404).send({ error: 'not_found' });
|
||||||
const base = reqBase(req);
|
const base = config.CONTROL_PLANE_PUBLIC_URL;
|
||||||
return reply.send({
|
return reply.send({
|
||||||
resource: parsed.data.resource,
|
resource: parsed.data.resource,
|
||||||
authorization_servers: [`${base}/oauth`],
|
authorization_servers: [`${base}/oauth`],
|
||||||
@ -259,12 +259,3 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reqBase(req: { protocol?: string; headers: Record<string, string | string[] | undefined> }): string {
|
|
||||||
const host =
|
|
||||||
(req.headers['x-forwarded-host'] as string | undefined) ??
|
|
||||||
(req.headers.host as string | undefined) ??
|
|
||||||
`localhost:${config.PORT}`;
|
|
||||||
const proto =
|
|
||||||
(req.headers['x-forwarded-proto'] as string | undefined) ?? req.protocol ?? 'http';
|
|
||||||
return `${proto}://${host}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -183,10 +183,12 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
.where(eq(buildLogs.buildId, buildId))
|
.where(eq(buildLogs.buildId, buildId))
|
||||||
.orderBy(buildLogs.timestamp);
|
.orderBy(buildLogs.timestamp);
|
||||||
for (const log of logs) {
|
for (const log of logs) {
|
||||||
|
const level: 'info' | 'warn' | 'error' =
|
||||||
|
log.level === 'warn' || log.level === 'error' ? log.level : 'info';
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'log',
|
type: 'log',
|
||||||
level: log.level,
|
level,
|
||||||
message: log.message,
|
message: log.message,
|
||||||
at: log.timestamp.toISOString(),
|
at: log.timestamp.toISOString(),
|
||||||
} satisfies BuildEvent),
|
} satisfies BuildEvent),
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
"bullmq": "5.34.5",
|
"bullmq": "5.34.5",
|
||||||
"drizzle-orm": "0.36.4",
|
"drizzle-orm": "0.36.4",
|
||||||
"ioredis": "5.4.1",
|
"ioredis": "5.4.1",
|
||||||
"zod": "3.23.8"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "22.10.2",
|
"@types/node": "22.10.2",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ const Env = z.object({
|
|||||||
RUNNER_PORT_RANGE_END: z.coerce.number().default(4999),
|
RUNNER_PORT_RANGE_END: z.coerce.number().default(4999),
|
||||||
CONTROL_PLANE_URL: z.string().default('http://host.docker.internal:4000'),
|
CONTROL_PLANE_URL: z.string().default('http://host.docker.internal:4000'),
|
||||||
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
|
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
|
||||||
|
OAUTH_ISSUER: z.string().optional(),
|
||||||
MODEL_GENERATE: z.string().default('claude-opus-4-7'),
|
MODEL_GENERATE: z.string().default('claude-opus-4-7'),
|
||||||
MODEL_FIX: z.string().default('claude-haiku-4-5-20251001'),
|
MODEL_FIX: z.string().default('claude-haiku-4-5-20251001'),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -61,6 +61,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
|
|
||||||
const PUBLIC_URL = process.env.PUBLIC_URL ?? 'http://localhost:3000';
|
const PUBLIC_URL = process.env.PUBLIC_URL ?? 'http://localhost:3000';
|
||||||
const CONTROL_PLANE_URL = process.env.CONTROL_PLANE_URL ?? 'http://host.docker.internal:4000';
|
const CONTROL_PLANE_URL = process.env.CONTROL_PLANE_URL ?? 'http://host.docker.internal:4000';
|
||||||
|
const OAUTH_ISSUER = process.env.OAUTH_ISSUER ?? CONTROL_PLANE_URL + '/oauth';
|
||||||
const PORT = Number.parseInt(process.env.PORT ?? '3000', 10);
|
const PORT = Number.parseInt(process.env.PORT ?? '3000', 10);
|
||||||
|
|
||||||
const server = new McpServer(
|
const server = new McpServer(
|
||||||
@ -76,7 +77,7 @@ app.get('/health', async () => ({ ok: true }));
|
|||||||
|
|
||||||
app.get('/.well-known/oauth-protected-resource', async () => ({
|
app.get('/.well-known/oauth-protected-resource', async () => ({
|
||||||
resource: PUBLIC_URL,
|
resource: PUBLIC_URL,
|
||||||
authorization_servers: [CONTROL_PLANE_URL + '/oauth'],
|
authorization_servers: [OAUTH_ISSUER],
|
||||||
bearer_methods_supported: ['header'],
|
bearer_methods_supported: ['header'],
|
||||||
scopes_supported: ${JSON.stringify(spec.scopes)},
|
scopes_supported: ${JSON.stringify(spec.scopes)},
|
||||||
}));
|
}));
|
||||||
@ -101,7 +102,7 @@ app.all('/mcp', async (request, reply) => {
|
|||||||
const token = auth.slice(7);
|
const token = auth.slice(7);
|
||||||
try {
|
try {
|
||||||
const { payload } = await jwtVerify(token, JWKS, {
|
const { payload } = await jwtVerify(token, JWKS, {
|
||||||
issuer: CONTROL_PLANE_URL + '/oauth',
|
issuer: OAUTH_ISSUER,
|
||||||
audience: PUBLIC_URL,
|
audience: PUBLIC_URL,
|
||||||
});
|
});
|
||||||
if (payload.aud !== PUBLIC_URL) {
|
if (payload.aud !== PUBLIC_URL) {
|
||||||
|
|||||||
@ -79,6 +79,7 @@ export const worker = new Worker<JobData>(
|
|||||||
...secrets,
|
...secrets,
|
||||||
PUBLIC_URL: publicUrl,
|
PUBLIC_URL: publicUrl,
|
||||||
CONTROL_PLANE_URL: config.CONTROL_PLANE_URL,
|
CONTROL_PLANE_URL: config.CONTROL_PLANE_URL,
|
||||||
|
OAUTH_ISSUER: `${config.CONTROL_PLANE_PUBLIC_URL}/oauth`,
|
||||||
PORT: '3000',
|
PORT: '3000',
|
||||||
SERVER_ID: serverId,
|
SERVER_ID: serverId,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,10 +7,10 @@
|
|||||||
"start": "tsx src/server.ts"
|
"start": "tsx src/server.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.0.4",
|
"@modelcontextprotocol/sdk": "1.29.0",
|
||||||
"fastify": "5.2.0",
|
"fastify": "5.2.0",
|
||||||
"jose": "5.9.6",
|
"jose": "5.9.6",
|
||||||
"zod": "3.23.8"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsx": "4.19.2",
|
"tsx": "4.19.2",
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const metadata: Metadata = {
|
|||||||
title: 'BuildMyMCPServer — Describe your tool. We host the server.',
|
title: 'BuildMyMCPServer — Describe your tool. We host the server.',
|
||||||
description:
|
description:
|
||||||
'From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready for Claude, Cursor & ChatGPT.',
|
'From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready for Claude, Cursor & ChatGPT.',
|
||||||
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'),
|
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3001'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
3
apps/web/next-env.d.ts
vendored
3
apps/web/next-env.d.ts
vendored
@ -1,2 +1,5 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --port 3000",
|
"dev": "next dev --port 3001",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start --port 3000",
|
"start": "next start --port 3001",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -18,7 +18,7 @@
|
|||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"tailwind-merge": "2.5.5",
|
"tailwind-merge": "2.5.5",
|
||||||
"zod": "3.23.8"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.0.0-beta.7",
|
"@tailwindcss/postcss": "4.0.0-beta.7",
|
||||||
|
|||||||
@ -2,16 +2,34 @@
|
|||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"lib": ["dom", "dom.iterable", "es2022"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"es2022"
|
||||||
|
],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"plugins": [{ "name": "next" }],
|
"plugins": [
|
||||||
"paths": {
|
{
|
||||||
"@/*": ["./*"]
|
"name": "next"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"allowJs": true
|
||||||
"exclude": ["node_modules"]
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: bmm
|
POSTGRES_PASSWORD: bmm
|
||||||
POSTGRES_DB: bmm
|
POSTGRES_DB: bmm
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5440:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- bmm_pg:/var/lib/postgresql/data
|
- bmm_pg:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@ -20,7 +20,7 @@ services:
|
|||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: bmm-redis
|
container_name: bmm-redis
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6390:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- bmm_redis:/data
|
- bmm_redis:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "3.23.8"
|
"zod": "3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "5.7.2"
|
"typescript": "5.7.2"
|
||||||
|
|||||||
3692
pnpm-lock.yaml
generated
Normal file
3692
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user