prepare( 'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?' ); $stmt->execute([$table, $column]); return (int)$stmt->fetchColumn() > 0; } function dbnMcpTableExists(PDO $db, string $table): bool { $stmt = $db->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?' ); $stmt->execute([$table]); return (int)$stmt->fetchColumn() > 0; } function dbnMcpRequireTables(PDO $db, array $tables): void { $missing = []; foreach ($tables as $table) { if (!dbnMcpTableExists($db, $table)) { $missing[] = $table; } } if ($missing !== []) { throw new RuntimeException( 'Missing required MCP tables: ' . implode(', ', $missing) . '. Apply the Caveau MCP and Azure Search migrations before running this script.' ); } } function dbnMcpUpsertFeature(PDO $db, int $clientId, string $feature, int $enabled): void { $stmt = $db->prepare( 'INSERT INTO client_feature_overrides (client_id, feature, enabled) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE enabled = VALUES(enabled)' ); $stmt->execute([$clientId, $feature, $enabled]); } try { $db = dbnToolsDb(); dbnMcpRequireTables($db, [ 'client_mcp_config', 'client_mcp_tokens', 'client_corpus_subscriptions', 'client_feature_overrides', ]); $client = dbnToolsFetchClient($db); if (!$client || empty($client['is_active'])) { throw new RuntimeException('Do Better Norge client tenant is not active or was not found.'); } $clientId = (int)$client['id']; $packageSlug = dbnToolsRequiredPackageSlug(); $package = dbnToolsFetchPackage($packageSlug, $db); if (!$package || empty($package['is_active'])) { throw new RuntimeException("Package {$packageSlug} is not active or was not found."); } if ((int)($package['corpus_id'] ?? 0) !== 1) { throw new RuntimeException("Package {$packageSlug} must point at corpus_id=1 for DBN MCP v1."); } $db->beginTransaction(); $stmt = $db->prepare( 'INSERT INTO client_corpus_subscriptions (client_id, package_id, is_active, source, subscribed_at) VALUES (?, ?, 1, ?, NOW()) ON DUPLICATE KEY UPDATE is_active = VALUES(is_active), source = VALUES(source), cancelled_at = NULL' ); $stmt->execute([$clientId, (int)$package['id'], 'dbn-mcp-v1']); $configColumns = [ 'client_id' => $clientId, 'is_enabled' => 1, 'endpoint_slug' => 'dobetter', ]; if (dbnMcpColumnExists($db, 'client_mcp_config', 'inspector_enabled')) { $configColumns['inspector_enabled'] = 1; } if (dbnMcpColumnExists($db, 'client_mcp_config', 'inspector_retention_days')) { $configColumns['inspector_retention_days'] = 30; } $names = array_keys($configColumns); $updates = implode(', ', array_map(static fn(string $name): string => "{$name} = VALUES({$name})", $names)); $stmt = $db->prepare( 'INSERT INTO client_mcp_config (' . implode(', ', $names) . ') VALUES (' . implode(', ', array_fill(0, count($names), '?')) . ') ON DUPLICATE KEY UPDATE ' . $updates ); $stmt->execute(array_values($configColumns)); dbnMcpUpsertFeature($db, $clientId, 'azure_search', 1); $existingToken = $db->prepare( "SELECT id, token_prefix, name, created_at FROM client_mcp_tokens WHERE client_id = ? AND is_active = 1 AND revoked_at IS NULL AND name = 'DBN MCP v1' ORDER BY id DESC LIMIT 1" ); $existingToken->execute([$clientId]); $tokenRow = $existingToken->fetch(PDO::FETCH_ASSOC); $plaintext = null; if (!$tokenRow) { $plaintext = 'dbn_mcp_' . bin2hex(random_bytes(32)); $tokenHash = hash('sha256', $plaintext); $prefix = substr($plaintext, 0, 16); $scopes = json_encode([ 'tools' => ['dbn.search_legal', 'dbn.compare_retrieval', 'dbn.list_sources', 'dbn.get_document'], 'backend' => ['azure-search', 'qdrant'], 'corpus_id' => [1], ], JSON_UNESCAPED_SLASHES); $stmt = $db->prepare( 'INSERT INTO client_mcp_tokens (client_id, token_hash, token_prefix, name, scopes, is_active, created_at) VALUES (?, ?, ?, ?, ?, 1, NOW())' ); $stmt->execute([$clientId, $tokenHash, $prefix, 'DBN MCP v1', $scopes]); $tokenRow = [ 'id' => (int)$db->lastInsertId(), 'token_prefix' => $prefix, 'name' => 'DBN MCP v1', 'created_at' => date('Y-m-d H:i:s'), ]; } $db->commit(); echo "DBN MCP configured.\n"; echo "Client: " . dbnToolsClientSlug() . " (#{$clientId})\n"; echo "Package: {$packageSlug} (#{$package['id']}) corpus_id={$package['corpus_id']}\n"; echo "MCP config: enabled endpoint_slug=dobetter\n"; echo "Azure Search feature: enabled\n"; echo "Token: {$tokenRow['name']} (#{$tokenRow['id']}) prefix={$tokenRow['token_prefix']}\n"; if ($plaintext !== null) { echo "Plaintext token (shown once): {$plaintext}\n"; } else { echo "Plaintext token: not shown; an active DBN MCP v1 token already exists.\n"; } } catch (Throwable $e) { if (isset($db) && $db instanceof PDO && $db->inTransaction()) { $db->rollBack(); } fwrite(STDERR, "DBN MCP setup failed: {$e->getMessage()}\n"); exit(1); }