README.md•9.52 kB
# @vibase/core
Easily read and mutate Postgres data using MCP.
## Overview
`@vibase/core` provides the foundational components for creating MCP (Model Context Protocol) servers on top of Postgres using simple configuration files.
- Run on your own machine with STDIO
- Deploy on lambda or Cloudflare workers
- Package in a [Claude Desktop Extension](https://www.anthropic.com/engineering/desktop-extensions)
## Features
- **YAML Configuration**: Define MCP tools using simple YAML configuration files
- **PostgreSQL Support**: Direct SQL execution against PostgreSQL databases with connection pooling
- **Safe SQL Generation**: Uses parameterized queries for safe SQL query construction
- **Type Safety**: Full TypeScript support with Zod validation
- **Connection Management**: Automatic connection pooling and cleanup
## Installation
```bash
npm install -g @vibase/core
```
## Quick Start
1. **Create a YAML configuration file** (`tools.yaml`):
```yaml
sources:
todo_db:
kind: postgres
connection_string: postgres://user:password@localhost:5432/todo_management
tools:
get_boards:
kind: postgres
source: todo_db
description: Retrieve all todo boards
statement: SELECT id, name, description FROM boards ORDER BY created_at DESC;
parameters: []
search_tasks:
kind: postgres
source: todo_db
description: Search tasks by title or description
parameters:
- name: search_term
type: string
description: Search term to match against task title or description
required: true
statement: |
SELECT t.id, t.title, t.priority, s.name as stage_name
FROM tasks t
JOIN stages s ON t.stage_id = s.id
WHERE t.title ILIKE '%' || $1 || '%' OR t.description ILIKE '%' || $1 || '%';
```
2. **Create and run the MCP server**:
```bash
npx @vibase/core tools.yaml --http 5555
```
Your MCP server now runs over streamable HTTP on http://localhost:5555
You can view options with ```--help```
## API Reference
Use Vibase with any TypeScript MCP server.
### Configuration
#### `loadConfigFromYaml(configPath: string): ToolboxConfig`
Loads and validates configuration from a YAML file.
**Parameters:**
- `configPath`: Path to the YAML configuration file
**Returns:**
- `ToolboxConfig`: Validated configuration object
**Throws:**
- Configuration validation errors
- File not found errors
- YAML parsing errors
#### `loadConfigFromYamlString(yamlContent: string): ToolboxConfig`
Loads and validates configuration from a YAML string.
**Parameters:**
- `yamlContent`: YAML configuration as a string
**Returns:**
- `ToolboxConfig`: Validated configuration object
**Throws:**
- Configuration validation errors
- YAML parsing errors
### Server Creation
#### `createMcpServerFromConfig(config: ToolboxConfig, options?: ServerOptions): { server: McpServer; cleanup: () => Promise<void>; plugins: PluginRegistry }`
Creates an MCP server from a validated configuration object.
**Parameters:**
- `config`: Validated configuration object
- `options`: Optional server configuration options
**Returns:**
- `server`: The MCP server instance
- `cleanup`: Async function to clean up database connections
- `plugins`: Plugin registry for registering custom plugins
#### `addToolsToMcpServer(server: McpServer, config: ToolboxConfig): { cleanup: () => Promise<void>; plugins: PluginRegistry }`
Adds tools from configuration to an existing MCP server. This is useful when you want to add tools to a server that was created separately.
**Parameters:**
- `server`: Existing MCP server instance
- `config`: Validated configuration object
**Returns:**
- `cleanup`: Async function to clean up database connections
- `plugins`: Plugin registry for registering custom plugins
**Example:**
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { addToolsToMcpServer, loadConfigFromYaml } from "@vibase/core";
// Create server manually
const server = new McpServer({
name: "My Custom Server",
version: "1.0.0",
});
// Load configuration and add tools
const config = loadConfigFromYaml("./config.yaml");
const { cleanup, plugins } = addToolsToMcpServer(server, config);
// Register plugins if needed
const authPlugin = {
name: "auth",
callbacks: {
beforeQuery: (context) => {
return { pgSettings: new Map([["role", "authenticated"]]) };
}
}
};
plugins.register(authPlugin);
// Connect transport and handle cleanup
process.on("SIGINT", async () => {
await cleanup();
process.exit(0);
});
```
## Configuration Format
### Sources
Define database connections:
```yaml
sources:
source-name:
kind: postgres
connection_string: postgres://user:password@host:port/database
```
### Tools
Define SQL-based tools:
```yaml
tools:
tool-name:
kind: postgres
source: source-name # References a source
description: Human-readable description
parameters:
- name: param_name
type: string | number | boolean
description: Parameter description
required: true | false # Optional, defaults to true
statement: SELECT * FROM table WHERE column = $1;
```
## Plugins API
`@vibase/core` includes a plugin system that allows you to modify query execution behavior. The most common use case is implementing authentication with JWT tokens and PostgreSQL Row Level Security (RLS).
### Plugin Registration
```typescript
import { createMcpServerFromConfig, loadConfigFromYaml } from "@vibase/core";
// Create server and get plugins registry
const config = loadConfigFromYaml("./config.yaml");
const { server, plugins } = createMcpServerFromConfig(config);
// Register a plugin
const myPlugin = {
name: "my-plugin",
callbacks: {
beforeQuery: (context) => {
// Plugin logic here
return { pgSettings: new Map([["role", "authenticated"]]) };
}
}
};
plugins.register(myPlugin);
```
### JWT Authentication Plugin Example
Here's how you can implement JWT bearer token authentication:
```typescript
import jwt from "jsonwebtoken";
function createBearerAuthPlugin(jwtSecret: string) {
return {
name: "bearer-auth",
callbacks: {
beforeQuery: ({ extra }) => {
const pgSettings = new Map();
try {
// Extract JWT from authInfo.token or Authorization header
let token = extra?.authInfo?.token;
if (!token) {
const authHeader = extra?.requestInfo?.headers?.authorization;
if (authHeader?.startsWith('Bearer ')) {
token = authHeader.replace(/^Bearer\s+/i, '');
}
}
if (!token) {
// No token - set anonymous role
pgSettings.set('role', 'anonymous');
return { pgSettings };
}
// Verify and decode JWT
const decoded = jwt.verify(token, jwtSecret);
// Set PostgreSQL session variables for RLS
pgSettings.set('role', 'authenticated');
pgSettings.set('jwt.claims.sub', decoded.sub);
pgSettings.set('jwt.claims.role', decoded.role);
return { pgSettings };
} catch (error) {
// JWT verification failed - set anonymous role
pgSettings.set('role', 'anonymous');
return { pgSettings };
}
}
}
};
}
// Register the plugin
const authPlugin = createBearerAuthPlugin(process.env.JWT_SECRET);
plugins.register(authPlugin);
```
### How Plugins Work
1. **Plugin Structure**: Plugins are objects with a `name` and `callbacks` for different hooks
2. **Plugin Registration**: Use `plugins.register(plugin)` to register a complete plugin
3. **Hook Execution**: Plugin callbacks run before each query (more hooks coming soon)
4. **Context Access**: Callbacks receive context including tool name, parsed arguments, and request metadata
5. **Session Variables**: Plugins return `pgSettings` Maps that become PostgreSQL session variables
6. **Automatic Transactions**: When `pgSettings` are present, queries run in transactions with variables applied
7. **RLS Integration**: SQL queries can access variables via `current_setting('jwt.claims.sub')`
### Plugin Management
```typescript
// Get all registered plugins
const allPlugins = plugins.getRegisteredPlugins();
// Get specific plugin by name
const authPlugin = plugins.getPlugin("bearer-auth");
```
### Row Level Security Integration
With the authentication plugin, your SQL queries can reference JWT claims:
```sql
-- Users can only see their own todos
CREATE POLICY user_todos ON todos
FOR ALL USING (user_id = current_setting('jwt.claims.sub')::uuid);
-- Query automatically filtered by RLS
SELECT * FROM todos; -- Only returns current user's todos
```
### Plugin Context
Plugin callbacks receive a context object with:
- `toolName`: Name of the tool being executed
- `toolConfig`: Tool configuration from YAML
- `pool`: Database connection pool
- `parsedArgs`: Validated tool arguments
- `extra`: MCP request metadata (authInfo, requestInfo, etc.)
- `query`: The SQL query being executed
## Security Considerations
- Always use parameterized queries (enforced by this package)
- Store database credentials securely (consider environment variables)
- Limit database user permissions to only what's needed
- Use SSL connections for production databases
- Validate and sanitize user input before using in queries
## Contributing
Contributions are welcome!
## License
Apache 2.0