MCP Server 2.0 (#452)
* MCP Server 2.0 - Updated application version from 2.23.14 to 2.24.0 in package.json. - MCP Server improved with new features - Migrated functionality from Dev Tools to MCP Server - Improved getMonitoringProvider not to crash application when misconfigured
This commit is contained in:
committed by
GitHub
parent
059408a70a
commit
f3ac595d06
@@ -1,228 +0,0 @@
|
||||
import { EnvMode } from '@/app/variables/lib/types';
|
||||
|
||||
import { getVariable } from '../variables/lib/env-scanner';
|
||||
|
||||
export function createConnectivityService(mode: EnvMode) {
|
||||
return new ConnectivityService(mode);
|
||||
}
|
||||
|
||||
class ConnectivityService {
|
||||
constructor(private mode: EnvMode = 'development') {}
|
||||
|
||||
async checkSupabaseConnectivity() {
|
||||
const url = await getVariable('NEXT_PUBLIC_SUPABASE_URL', this.mode);
|
||||
|
||||
if (!url) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Supabase URL found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
const anonKey =
|
||||
(await getVariable('NEXT_PUBLIC_SUPABASE_ANON_KEY', this.mode)) ||
|
||||
(await getVariable('NEXT_PUBLIC_SUPABASE_PUBLIC_KEY', this.mode));
|
||||
|
||||
if (!anonKey) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Supabase Anon Key found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${url}/auth/v1/health`, {
|
||||
headers: {
|
||||
apikey: anonKey,
|
||||
Authorization: `Bearer ${anonKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message:
|
||||
'Failed to connect to Supabase. The Supabase Anon Key or URL is not valid.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'Connected to Supabase',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: `Failed to connect to Supabase. ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async checkSupabaseAdminConnectivity() {
|
||||
const url = await getVariable('NEXT_PUBLIC_SUPABASE_URL', this.mode);
|
||||
|
||||
if (!url) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Supabase URL found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
const endpoint = `${url}/rest/v1/accounts`;
|
||||
|
||||
const apikey =
|
||||
(await getVariable('NEXT_PUBLIC_SUPABASE_ANON_KEY', this.mode)) ||
|
||||
(await getVariable('NEXT_PUBLIC_SUPABASE_PUBLIC_KEY', this.mode));
|
||||
|
||||
if (!apikey) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Supabase Anon Key found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
const adminKey =
|
||||
(await getVariable('SUPABASE_SERVICE_ROLE_KEY', this.mode)) ||
|
||||
(await getVariable('SUPABASE_SECRET_KEY', this.mode));
|
||||
|
||||
if (!adminKey) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Supabase Service Role Key found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
apikey: adminKey,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message:
|
||||
'Failed to connect to Supabase Admin. The Supabase Service Role Key is not valid.',
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message:
|
||||
'No accounts found in Supabase Admin. The data may not be seeded. Please run `pnpm run supabase:web:reset` to reset the database.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'Connected to Supabase Admin',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: `Failed to connect to Supabase Admin. ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async checkStripeWebhookEndpoints() {
|
||||
const secretKey = await getVariable('STRIPE_SECRET_KEY', this.mode);
|
||||
|
||||
if (!secretKey) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Stripe Secret Key found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
const webhooksSecret = await getVariable(
|
||||
'STRIPE_WEBHOOK_SECRET',
|
||||
this.mode,
|
||||
);
|
||||
|
||||
if (!webhooksSecret) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Webhooks secret found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
const url = `https://api.stripe.com`;
|
||||
|
||||
const request = await fetch(`${url}/v1/webhook_endpoints`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!request.ok) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message:
|
||||
'Failed to connect to Stripe. The Stripe Webhook Secret is not valid.',
|
||||
};
|
||||
}
|
||||
|
||||
const webhooksResponse = await request.json();
|
||||
const webhooks = webhooksResponse.data ?? [];
|
||||
|
||||
if (webhooks.length === 0) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No webhooks found in Stripe',
|
||||
};
|
||||
}
|
||||
|
||||
const allWebhooksShareTheSameSecret = webhooks.every(
|
||||
(webhook: { secret: string }) => webhook.secret === webhooksSecret,
|
||||
);
|
||||
|
||||
if (!allWebhooksShareTheSameSecret) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'All webhooks do not share the same secret',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'All webhooks share the same Webhooks secret',
|
||||
};
|
||||
}
|
||||
|
||||
async checkStripeConnected() {
|
||||
const secretKey = await getVariable('STRIPE_SECRET_KEY', this.mode);
|
||||
|
||||
if (!secretKey) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: 'No Stripe Secret Key found in environment variables',
|
||||
};
|
||||
}
|
||||
|
||||
const url = `https://api.stripe.com`;
|
||||
|
||||
const request = await fetch(`${url}/v1/prices`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!request.ok) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message:
|
||||
'Failed to connect to Stripe. The Stripe Secret Key is not valid.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success' as const,
|
||||
message: 'Connected to Stripe',
|
||||
};
|
||||
}
|
||||
}
|
||||
154
apps/dev-tool/app/lib/prerequisites-dashboard.loader.ts
Normal file
154
apps/dev-tool/app/lib/prerequisites-dashboard.loader.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { access, readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import {
|
||||
type KitPrerequisitesDeps,
|
||||
createKitPrerequisitesService,
|
||||
} from '@kit/mcp-server/prerequisites';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export async function loadDashboardKitPrerequisites() {
|
||||
const rootPath = await findWorkspaceRoot(process.cwd());
|
||||
const service = createKitPrerequisitesService(
|
||||
createKitPrerequisitesDeps(rootPath),
|
||||
);
|
||||
return service.check({});
|
||||
}
|
||||
|
||||
function createKitPrerequisitesDeps(rootPath: string): KitPrerequisitesDeps {
|
||||
return {
|
||||
async getVariantFamily() {
|
||||
const variant = await resolveVariant(rootPath);
|
||||
return variant.includes('supabase') ? 'supabase' : 'orm';
|
||||
},
|
||||
async executeCommand(command: string, args: string[]) {
|
||||
const result = await executeWithFallback(rootPath, command, args);
|
||||
|
||||
return {
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
exitCode: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function executeWithFallback(
|
||||
rootPath: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
) {
|
||||
try {
|
||||
return await execFileAsync(command, args, {
|
||||
cwd: rootPath,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isLocalCliCandidate(command)) {
|
||||
const localBinCandidates = [
|
||||
join(rootPath, 'node_modules', '.bin', command),
|
||||
join(rootPath, 'apps', 'web', 'node_modules', '.bin', command),
|
||||
];
|
||||
|
||||
for (const localBin of localBinCandidates) {
|
||||
try {
|
||||
return await execFileAsync(localBin, args, {
|
||||
cwd: rootPath,
|
||||
});
|
||||
} catch {
|
||||
// Try next local binary candidate.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await execFileAsync('pnpm', ['exec', command, ...args], {
|
||||
cwd: rootPath,
|
||||
});
|
||||
} catch {
|
||||
return execFileAsync(
|
||||
'pnpm',
|
||||
['--filter', 'web', 'exec', command, ...args],
|
||||
{
|
||||
cwd: rootPath,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalCliCandidate(command: string) {
|
||||
return command === 'supabase' || command === 'stripe';
|
||||
}
|
||||
|
||||
async function resolveVariant(rootPath: string) {
|
||||
const configPath = join(rootPath, '.makerkit', 'config.json');
|
||||
|
||||
try {
|
||||
await access(configPath);
|
||||
const config = JSON.parse(await readFile(configPath, 'utf8')) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const variant =
|
||||
readString(config, 'variant') ??
|
||||
readString(config, 'template') ??
|
||||
readString(config, 'kitVariant');
|
||||
|
||||
if (variant) {
|
||||
return variant;
|
||||
}
|
||||
} catch {
|
||||
// Fall through to heuristic.
|
||||
}
|
||||
|
||||
if (await pathExists(join(rootPath, 'apps', 'web', 'supabase'))) {
|
||||
return 'next-supabase';
|
||||
}
|
||||
|
||||
return 'next-drizzle';
|
||||
}
|
||||
|
||||
function readString(obj: Record<string, unknown>, key: string) {
|
||||
const value = obj[key];
|
||||
return typeof value === 'string' && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
async function pathExists(path: string) {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function findWorkspaceRoot(startPath: string) {
|
||||
let current = startPath;
|
||||
|
||||
for (let depth = 0; depth < 6; depth++) {
|
||||
const workspaceManifest = join(current, 'pnpm-workspace.yaml');
|
||||
|
||||
try {
|
||||
await access(workspaceManifest);
|
||||
return current;
|
||||
} catch {
|
||||
// Continue to parent path.
|
||||
}
|
||||
|
||||
const parent = join(current, '..');
|
||||
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return startPath;
|
||||
}
|
||||
116
apps/dev-tool/app/lib/status-dashboard.loader.ts
Normal file
116
apps/dev-tool/app/lib/status-dashboard.loader.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { access, readFile, stat } from 'node:fs/promises';
|
||||
import { Socket } from 'node:net';
|
||||
import { join } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import {
|
||||
type KitStatusDeps,
|
||||
createKitStatusService,
|
||||
} from '@kit/mcp-server/status';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export async function loadDashboardKitStatus() {
|
||||
const rootPath = await findWorkspaceRoot(process.cwd());
|
||||
const service = createKitStatusService(createKitStatusDeps(rootPath));
|
||||
return service.getStatus({});
|
||||
}
|
||||
|
||||
function createKitStatusDeps(rootPath: string): KitStatusDeps {
|
||||
return {
|
||||
rootPath,
|
||||
async readJsonFile(path: string): Promise<unknown> {
|
||||
const filePath = join(rootPath, path);
|
||||
const content = await readFile(filePath, 'utf8');
|
||||
return JSON.parse(content) as unknown;
|
||||
},
|
||||
async pathExists(path: string) {
|
||||
const fullPath = join(rootPath, path);
|
||||
|
||||
try {
|
||||
await access(fullPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async isDirectory(path: string) {
|
||||
const fullPath = join(rootPath, path);
|
||||
|
||||
try {
|
||||
const stats = await stat(fullPath);
|
||||
return stats.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async executeCommand(command: string, args: string[]) {
|
||||
const result = await execFileAsync(command, args, {
|
||||
cwd: rootPath,
|
||||
});
|
||||
|
||||
return {
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
exitCode: 0,
|
||||
};
|
||||
},
|
||||
async isPortOpen(port: number) {
|
||||
return checkPort(port);
|
||||
},
|
||||
getNodeVersion() {
|
||||
return process.version;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function findWorkspaceRoot(startPath: string) {
|
||||
let current = startPath;
|
||||
|
||||
for (let depth = 0; depth < 6; depth++) {
|
||||
const workspaceManifest = join(current, 'pnpm-workspace.yaml');
|
||||
|
||||
try {
|
||||
await access(workspaceManifest);
|
||||
return current;
|
||||
} catch {
|
||||
// Continue to parent path.
|
||||
}
|
||||
|
||||
const parent = join(current, '..');
|
||||
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return startPath;
|
||||
}
|
||||
|
||||
async function checkPort(port: number) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const socket = new Socket();
|
||||
|
||||
socket.setTimeout(200);
|
||||
|
||||
socket.once('connect', () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.once('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.once('error', () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(port, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user