/**
* db-mcp - OAuth Scopes
*
* Scope definitions and enforcement utilities for
* granular access control.
*
* Scope Patterns:
* - read : Read-only access to all databases
* - write : Read and write access to all databases
* - admin : Full administrative access
* - db:{name} : Access to specific database only
* - table:{db}:{table} : Access to specific table only
*/
import type { ToolGroup } from "../types/index.js";
import { TOOL_GROUPS } from "../filtering/ToolFilter.js";
// =============================================================================
// Scope Constants
// =============================================================================
/**
* Base scopes supported by the server
*/
export const BASE_SCOPES = ["read", "write", "admin"] as const;
/**
* Scope patterns (regex patterns for validation)
*/
export const SCOPE_PATTERNS = {
/** Read-only access */
READ: "read",
/** Read and write access */
WRITE: "write",
/** Full admin access */
ADMIN: "admin",
/** Database-specific access pattern */
DATABASE: /^db:([a-zA-Z0-9_-]+)$/,
/** Table-specific access pattern */
TABLE: /^table:([a-zA-Z0-9_-]+):([a-zA-Z0-9_-]+)$/,
} as const;
/**
* All supported scope patterns for metadata
*/
export const SUPPORTED_SCOPES = [
"read",
"write",
"admin",
"db:{database}",
"table:{database}:{table}",
] as const;
// =============================================================================
// Scope to Tool Group Mapping
// =============================================================================
/**
* Tool groups accessible with read scope (read-only operations)
*/
export const READ_SCOPE_GROUPS: ToolGroup[] = [
"core", // read_query, list_tables, describe_table, etc.
];
/**
* Tool groups accessible with write scope (read + write operations)
*/
export const WRITE_SCOPE_GROUPS: ToolGroup[] = [
...READ_SCOPE_GROUPS,
"json", // JSON operations
"text", // Text processing
"stats", // Statistical analysis
"vector", // Vector operations
];
/**
* Tool groups accessible with admin scope (all operations)
*/
export const ADMIN_SCOPE_GROUPS: ToolGroup[] = [
...WRITE_SCOPE_GROUPS,
"admin", // Administration
];
/**
* Read-only tools within the core group
* (used when scope is 'read' to filter write operations)
*/
export const READ_ONLY_TOOLS = new Set([
"execute_query", // If used with SELECT only
"read_query",
"list_tables",
"describe_table",
"list_schemas",
"get_schema",
"health_check",
"connection_status",
"database_stats",
"active_queries",
"resource_usage",
"analyze_query",
"explain_query",
"query_plan",
]);
/**
* Write tools that require 'write' scope
*/
export const WRITE_TOOLS = new Set([
"write_query",
"create_table",
"drop_table",
"json_insert",
"json_replace",
"json_remove",
"json_set",
"create_fts_index",
"create_vector_index",
"create_spatial_index",
"create_index",
"drop_index",
"reindex",
]);
/**
* Admin tools that require 'admin' scope
*/
export const ADMIN_TOOLS = new Set([
"vacuum_database",
"analyze_tables",
"pragma_get",
"pragma_set",
"extension_list",
"extension_install",
"optimize",
"backup_database",
"restore_database",
"backup_table",
"export_data",
]);
// =============================================================================
// Scope Parsing
// =============================================================================
/**
* Parse a scope string (space-delimited) into an array
*/
export function parseScopes(scopeString: string): string[] {
return scopeString
.split(/\s+/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
/**
* Parse a database-specific scope
* @returns The database name or null if not a database scope
*/
export function parseDatabaseScope(scope: string): string | null {
const match = SCOPE_PATTERNS.DATABASE.exec(scope);
return match?.[1] ?? null;
}
/**
* Parse a table-specific scope
* @returns Object with database and table names, or null if not a table scope
*/
export function parseTableScope(
scope: string,
): { database: string; table: string } | null {
const match = SCOPE_PATTERNS.TABLE.exec(scope);
const database = match?.[1];
const table = match?.[2];
if (database !== undefined && table !== undefined) {
return { database, table };
}
return null;
}
// =============================================================================
// Scope Validation
// =============================================================================
/**
* Check if a scope is valid (matches known patterns)
*/
export function isValidScope(scope: string): boolean {
// Check base scopes
if ((BASE_SCOPES as readonly string[]).includes(scope)) {
return true;
}
// Check database pattern
if (SCOPE_PATTERNS.DATABASE.test(scope)) {
return true;
}
// Check table pattern
if (SCOPE_PATTERNS.TABLE.test(scope)) {
return true;
}
return false;
}
/**
* Check if scopes include admin access
*/
export function hasAdminScope(scopes: string[]): boolean {
return scopes.includes("admin");
}
/**
* Check if scopes include write access
*/
export function hasWriteScope(scopes: string[]): boolean {
return scopes.includes("write") || hasAdminScope(scopes);
}
/**
* Check if scopes include read access
*/
export function hasReadScope(scopes: string[]): boolean {
return scopes.includes("read") || hasWriteScope(scopes);
}
// =============================================================================
// Scope Enforcement
// =============================================================================
/**
* Check if a scope grants access to a specific tool
*/
export function scopeGrantsToolAccess(
scope: string,
toolName: string,
): boolean {
// Admin scope grants access to all tools
if (scope === "admin") {
return true;
}
// Write scope grants access to write tools and below
if (scope === "write") {
if (ADMIN_TOOLS.has(toolName)) {
return false;
}
return true;
}
// Read scope only grants read-only tools
if (scope === "read") {
return READ_ONLY_TOOLS.has(toolName);
}
// Database/table scopes don't directly affect tool access
// They are used for filtering data, not tools
return false;
}
/**
* Check if any of the scopes grants access to a tool
*/
export function scopesGrantToolAccess(
scopes: string[],
toolName: string,
): boolean {
return scopes.some((scope) => scopeGrantsToolAccess(scope, toolName));
}
/**
* Check if a scope grants access to a specific database
*/
export function scopeGrantsDatabaseAccess(
scope: string,
databaseName: string,
): boolean {
// Admin and write scopes grant access to all databases
if (scope === "admin" || scope === "write" || scope === "read") {
return true;
}
// Check database-specific scope
const dbName = parseDatabaseScope(scope);
if (dbName && dbName === databaseName) {
return true;
}
// Check table scope (grants access to the database of the table)
const tableScope = parseTableScope(scope);
if (tableScope?.database === databaseName) {
return true;
}
return false;
}
/**
* Check if any of the scopes grants access to a database
*/
export function scopesGrantDatabaseAccess(
scopes: string[],
databaseName: string,
): boolean {
return scopes.some((scope) => scopeGrantsDatabaseAccess(scope, databaseName));
}
/**
* Check if a scope grants access to a specific table
*/
export function scopeGrantsTableAccess(
scope: string,
databaseName: string,
tableName: string,
): boolean {
// Admin and write scopes grant access to all tables
if (scope === "admin" || scope === "write" || scope === "read") {
return true;
}
// Database scope grants access to all tables in that database
const dbName = parseDatabaseScope(scope);
if (dbName && dbName === databaseName) {
return true;
}
// Check table-specific scope
const tableScope = parseTableScope(scope);
if (tableScope?.database === databaseName && tableScope.table === tableName) {
return true;
}
return false;
}
/**
* Check if any of the scopes grants access to a table
*/
export function scopesGrantTableAccess(
scopes: string[],
databaseName: string,
tableName: string,
): boolean {
return scopes.some((scope) =>
scopeGrantsTableAccess(scope, databaseName, tableName),
);
}
// =============================================================================
// Tool Group Utilities
// =============================================================================
/**
* Get the required minimum scope for a tool group
*/
export function getRequiredScopeForGroup(group: ToolGroup): string {
if (
ADMIN_SCOPE_GROUPS.includes(group) &&
!WRITE_SCOPE_GROUPS.includes(group)
) {
return "admin";
}
if (
WRITE_SCOPE_GROUPS.includes(group) &&
!READ_SCOPE_GROUPS.includes(group)
) {
return "write";
}
return "read";
}
/**
* Get the required minimum scope for a tool
*/
export function getRequiredScopeForTool(toolName: string): string {
if (ADMIN_TOOLS.has(toolName)) {
return "admin";
}
if (WRITE_TOOLS.has(toolName)) {
return "write";
}
return "read";
}
/**
* Get tool groups accessible with given scopes
*/
export function getAccessibleToolGroups(scopes: string[]): ToolGroup[] {
if (hasAdminScope(scopes)) {
return [...ADMIN_SCOPE_GROUPS];
}
if (hasWriteScope(scopes)) {
return [...WRITE_SCOPE_GROUPS];
}
if (hasReadScope(scopes)) {
return [...READ_SCOPE_GROUPS];
}
return [];
}
/**
* Get all tools accessible with given scopes
*/
export function getAccessibleTools(scopes: string[]): string[] {
const groups = getAccessibleToolGroups(scopes);
const allTools: string[] = [];
for (const group of groups) {
const groupTools = TOOL_GROUPS[group] ?? [];
for (const tool of groupTools) {
// For read scope, only include read-only tools
if (hasReadScope(scopes) && !hasWriteScope(scopes)) {
if (READ_ONLY_TOOLS.has(tool)) {
allTools.push(tool);
}
} else {
allTools.push(tool);
}
}
}
return [...new Set(allTools)];
}