Skip to main content
Glama
mrwyndham

PocketBase MCP Server

index.ts33.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import PocketBase from 'pocketbase'; class PocketBaseServer { private server: Server; private pb: PocketBase; constructor() { this.server = new Server( { name: 'pocketbase-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); // Initialize PocketBase client const url = process.env.POCKETBASE_URL; if (!url) { throw new Error('POCKETBASE_URL environment variable is required'); } this.pb = new PocketBase(url); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create_collection', description: 'Create a new collection in PocketBase note never use created and updated because these are already created', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Unique collection name (used as a table name for the records table)', }, type: { type: 'string', description: 'Type of the collection', enum: ['base', 'view', 'auth'], default: 'base', }, fields: { type: 'array', description: 'List with the collection fields', items: { type: 'object', properties: { name: { type: 'string', description: 'Field name' }, type: { type: 'string', description: 'Field type', enum: ['bool', 'date', 'number', 'text', 'email', 'url', 'editor', 'autodate', 'select', 'file', 'relation', 'json'] }, required: { type: 'boolean', description: 'Is field required?' }, values: { type: 'array', items: { type: 'string' }, description: 'Allowed values for select type fields', }, collectionId: { type: 'string', description: 'Collection ID for relation type fields' } }, }, }, createRule: { type: 'string', description: 'API rule for creating records', }, updateRule: { type: 'string', description: 'API rule for updating records', }, deleteRule: { type: 'string', description: 'API rule for deleting records', }, listRule: { type: 'string', description: 'API rule for listing and viewing records', }, viewRule: { type: 'string', description: 'API rule for viewing a single record', }, viewQuery: { type: 'string', description: 'SQL query for view collections', }, passwordAuth: { type: 'object', description: 'Password authentication options', properties: { enabled: { type: 'boolean', description: 'Is password authentication enabled?' }, identityFields: { type: 'array', items: { type: 'string' }, description: 'Fields used for identity in password authentication', }, }, }, }, required: ['name', 'fields'], }, }, { name: 'update_collection', description: 'Update an existing collection in PocketBase (admin only)', inputSchema: { type: 'object', properties: { collectionIdOrName: { type: 'string', description: 'ID or name of the collection to update', }, name: { type: 'string', description: 'New unique collection name', }, type: { type: 'string', description: 'Type of the collection', enum: ['base', 'view', 'auth'], }, fields: { type: 'array', description: 'List with the new collection fields. If not empty, the old schema will be replaced with the new one.', items: { type: 'object', properties: { name: { type: 'string', description: 'Field name' }, type: { type: 'string', description: 'Field type', enum: ['bool', 'date', 'number', 'text', 'email', 'url', 'editor', 'autodate', 'select', 'file', 'relation', 'json'] }, required: { type: 'boolean', description: 'Is field required?' }, values: { type: 'array', items: { type: 'string' }, description: 'Allowed values for select type fields', }, collectionId: { type: 'string', description: 'Collection ID for relation type fields' } }, }, }, createRule: { type: 'string', description: 'API rule for creating records', }, updateRule: { type: 'string', description: 'API rule for updating records', }, deleteRule: { type: 'string', description: 'API rule for deleting records', }, listRule: { type: 'string', description: 'API rule for listing and viewing records', }, viewRule: { type: 'string', description: 'API rule for viewing a single record', }, viewQuery: { type: 'string', description: 'SQL query for view collections', }, passwordAuth: { type: 'object', description: 'Password authentication options', properties: { enabled: { type: 'boolean', description: 'Is password authentication enabled?' }, identityFields: { type: 'array', items: { type: 'string' }, description: 'Fields used for identity in password authentication', }, }, }, }, required: ['collectionIdOrName'], }, }, { name: 'create_record', description: 'Create a new record in a collection', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, data: { type: 'object', description: 'Record data', }, }, required: ['collection', 'data'], }, }, { name: 'list_records', description: 'List records from a collection with optional filters', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, filter: { type: 'string', description: 'Filter query', }, sort: { type: 'string', description: 'Sort field and direction', }, page: { type: 'number', description: 'Page number', }, perPage: { type: 'number', description: 'Items per page', }, }, required: ['collection'], }, }, { name: 'update_record', description: 'Update an existing record', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, id: { type: 'string', description: 'Record ID', }, data: { type: 'object', description: 'Updated record data', }, }, required: ['collection', 'id', 'data'], }, }, { name: 'delete_record', description: 'Delete a record', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, id: { type: 'string', description: 'Record ID', }, }, required: ['collection', 'id'], }, }, { name: 'list_auth_methods', description: 'List all available authentication methods', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } } } }, { name: 'authenticate_user', description: 'Authenticate a user with email and password', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email', }, password: { type: 'string', description: 'User password', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' }, isAdmin: { type: 'boolean', description: 'Whether to authenticate as an admin (uses _superusers collection)', default: false } }, required: ['email', 'password'], }, }, { name: 'authenticate_with_oauth2', description: 'Authenticate a user with OAuth2', inputSchema: { type: 'object', properties: { provider: { type: 'string', description: 'OAuth2 provider name (e.g., google, facebook, github)', }, code: { type: 'string', description: 'The authorization code returned from the OAuth2 provider', }, codeVerifier: { type: 'string', description: 'PKCE code verifier', }, redirectUrl: { type: 'string', description: 'The redirect URL used in the OAuth2 flow', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['provider', 'code', 'codeVerifier', 'redirectUrl'], }, }, { name: 'authenticate_with_otp', description: 'Authenticate a user with one-time password', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['email'], }, }, { name: 'auth_refresh', description: 'Refresh authentication token', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } } }, }, { name: 'request_verification', description: 'Request email verification', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['email'], }, }, { name: 'confirm_verification', description: 'Confirm email verification with token', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'Verification token', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['token'], }, }, { name: 'request_password_reset', description: 'Request password reset', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['email'], }, }, { name: 'confirm_password_reset', description: 'Confirm password reset with token', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'Reset token', }, password: { type: 'string', description: 'New password', }, passwordConfirm: { type: 'string', description: 'Confirm new password', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['token', 'password', 'passwordConfirm'], }, }, { name: 'request_email_change', description: 'Request email change', inputSchema: { type: 'object', properties: { newEmail: { type: 'string', description: 'New email address', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['newEmail'], }, }, { name: 'confirm_email_change', description: 'Confirm email change with token', inputSchema: { type: 'object', properties: { token: { type: 'string', description: 'Email change token', }, password: { type: 'string', description: 'Current password for confirmation', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['token', 'password'], }, }, { name: 'impersonate_user', description: 'Impersonate another user (admin only)', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'ID of the user to impersonate', }, collectionIdOrName: { type: 'string', description: 'Collection name or id (default: users)', default: 'users' }, duration: { type: 'number', description: 'Token expirey time (default: 3600)', default: 3600 } }, required: ['id'], }, }, { name: 'create_user', description: 'Create a new user account', inputSchema: { type: 'object', properties: { email: { type: 'string', description: 'User email', }, password: { type: 'string', description: 'User password', }, passwordConfirm: { type: 'string', description: 'Password confirmation', }, name: { type: 'string', description: 'User name', }, collection: { type: 'string', description: 'Collection name (default: users)', default: 'users' } }, required: ['email', 'password', 'passwordConfirm'], }, }, { name: 'get_collection', description: 'Get details for a collection', inputSchema: { type: 'object', properties: { collectionIdOrName: { type: 'string', description: 'ID or name of the collection to view', }, fields: { type: 'string', description: 'Comma separated string of the fields to return in the JSON response', }, }, required: ['collectionIdOrName'], }, }, { name: 'backup_database', description: 'Create a backup of the PocketBase database', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'backup name', }, }, }, }, { name: 'import_data', description: 'Import data into a collection', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, data: { type: 'array', description: 'Array of records to import', items: { type: 'object', }, }, mode: { type: 'string', enum: ['create', 'update', 'upsert'], description: 'Import mode (default: create)', }, }, required: ['collection', 'data'], }, }, { name: 'list_collections', description: 'List all collections in PocketBase', inputSchema: { type: 'object', properties: { filter: { type: 'string', description: 'Filter query for collections', }, sort: { type: 'string', description: 'Sort order for collections', }, }, }, }, { name: 'delete_collection', description: 'Delete a collection from PocketBase (admin only)', inputSchema: { type: 'object', properties: { collectionIdOrName: { type: 'string', description: 'ID or name of the collection to delete', }, }, required: ['collectionIdOrName'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case 'create_collection': return await this.createCollection(request.params.arguments); case 'update_collection': return await this.updateCollection(request.params.arguments); case 'create_record': return await this.createRecord(request.params.arguments); case 'list_records': return await this.listRecords(request.params.arguments); case 'update_record': return await this.updateRecord(request.params.arguments); case 'delete_record': return await this.deleteRecord(request.params.arguments); case 'authenticate_user': return await this.authenticateUser(request.params.arguments); case 'create_user': return await this.createUser(request.params.arguments); case 'get_collection': return await this.getCollection(request.params.arguments); case 'backup_database': return await this.backupDatabase(request.params.arguments); case 'list_collections': return await this.listCollections(request.params.arguments); case 'delete_collection': return await this.deleteCollection(request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error: unknown) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `PocketBase error: ${pocketbaseErrorMessage(error)}` ); } }); } private async createCollection(args: any) { try { // Authenticate with PocketBase await this.pb.collection("_superusers").authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL ?? '', process.env.POCKETBASE_ADMIN_PASSWORD ?? ''); const defaultFields = [ { hidden: false, id: "autodate_created", name: "created", onCreate: true, onUpdate: false, presentable: false, system: false, type: "autodate" }, { hidden: false, id: "autodate_updated", name: "updated", onCreate: true, onUpdate: true, presentable: false, system: false, type: "autodate" } ]; const collectionData = { ...args, fields: [...(args.fields || []), ...defaultFields] }; const result = await this.pb.collections.create(collectionData as any); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to create collection: ${pocketbaseErrorMessage(error)}` ); } } private async updateCollection(args: any) { try { // Authenticate with PocketBase as admin await this.pb.collection("_superusers").authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL ?? '', process.env.POCKETBASE_ADMIN_PASSWORD ?? ''); const { collectionIdOrName, ...updateData } = args; const result = await this.pb.collections.update(collectionIdOrName, updateData as any); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to update collection: ${pocketbaseErrorMessage(error)}` ); } } private async createRecord(args: any) { try { const result = await this.pb.collection(args.collection).create(args.data); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to create record: ${pocketbaseErrorMessage(error)}` ); } } private async listRecords(args: any) { try { const options: any = {}; if (args.filter) options.filter = args.filter; if (args.sort) options.sort = args.sort; if (args.page) options.page = args.page; if (args.perPage) options.perPage = args.perPage; const result = await this.pb.collection(args.collection).getList( options.page || 1, options.perPage || 50, { filter: options.filter, sort: options.sort, } ); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to list records: ${pocketbaseErrorMessage(error)}` ); } } private async updateRecord(args: any) { try { const result = await this.pb .collection(args.collection) .update(args.id, args.data); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to update record: ${pocketbaseErrorMessage(error)}` ); } } private async deleteRecord(args: any) { try { await this.pb.collection(args.collection).delete(args.id); return { content: [ { type: 'text', text: `Successfully deleted record ${args.id} from collection ${args.collection}`, }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to delete record: ${pocketbaseErrorMessage(error)}` ); } } private async authenticateUser(args: any) { try { // Use _superusers collection for admin authentication const collection = args.isAdmin ? '_superusers' : (args.collection || 'users'); // For admin authentication, use environment variables if email/password not provided const email = args.isAdmin && !args.email ? process.env.POCKETBASE_ADMIN_EMAIL : args.email; const password = args.isAdmin && !args.password ? process.env.POCKETBASE_ADMIN_PASSWORD : args.password; if (!email || !password) { throw new Error('Email and password are required for authentication'); } const authData = await this.pb .collection(collection) .authWithPassword(email, password); return { content: [ { type: 'text', text: JSON.stringify(authData, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Authentication failed: ${pocketbaseErrorMessage(error)}` ); } } private async createUser(args: any) { try { const collection = args.collection || 'users'; const result = await this.pb.collection(collection).create({ email: args.email, password: args.password, passwordConfirm: args.passwordConfirm, name: args.name, }); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to create user: ${pocketbaseErrorMessage(error)}` ); } } private async getCollection(args: any) { try { // Authenticate with PocketBase await this.pb.collection("_superusers").authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL ?? '', process.env.POCKETBASE_ADMIN_PASSWORD ?? ''); // Get collection details const collection = await this.pb.collections.getOne(args.collectionIdOrName, { fields: args.fields }); return { content: [ { type: 'text', text: JSON.stringify(collection, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to get collection: ${pocketbaseErrorMessage(error)}` ); } } private async backupDatabase(args: any) { try { // Authenticate with PocketBase await this.pb.collection("_superusers").authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL ?? '', process.env.POCKETBASE_ADMIN_PASSWORD ?? ''); // Create a new backup const backupResult = await this.pb.backups.create(args.name ?? '', {}); return { content: [ { type: 'text', text: JSON.stringify(backupResult, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to backup database: ${pocketbaseErrorMessage(error)}` ); } } private async listCollections(args: any) { try { // Authenticate with PocketBase await this.pb.collection("_superusers").authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL ?? '', process.env.POCKETBASE_ADMIN_PASSWORD ?? ''); // Fetch collections based on provided arguments let collections; if (args.filter) { collections = await this.pb.collections.getFirstListItem(args.filter); } else if (args.sort) { collections = await this.pb.collections.getFullList({ sort: args.sort }); } else { collections = await this.pb.collections.getList(1, 100); } return { content: [ { type: 'text', text: JSON.stringify(collections, null, 2), }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to list collections: ${pocketbaseErrorMessage(error)}` ); } } private async deleteCollection(args: any) { try { // Authenticate with PocketBase as admin (required for collection deletion) await this.pb.collection("_superusers").authWithPassword(process.env.POCKETBASE_ADMIN_EMAIL ?? '', process.env.POCKETBASE_ADMIN_PASSWORD ?? ''); // Delete the collection await this.pb.collections.delete(args.collectionIdOrName); return { content: [ { type: 'text', text: `Successfully deleted collection ${args.collectionIdOrName}`, }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to delete collection: ${pocketbaseErrorMessage(error)}` ); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('PocketBase MCP server running on stdio'); } } const server = new PocketBaseServer(); server.run().catch(console.error); export function flattenErrors(errors: unknown): string[] { if (Array.isArray(errors)) { return errors.flatMap(flattenErrors); } else if (typeof errors === "object" && errors !== null) { const errorObject = errors as Record<string, any>; // Handle objects with message property directly if (errorObject.message) { return [errorObject.message, ...flattenErrors(errorObject.data || {})]; } // Handle nested objects with code/message structure if (errorObject.data) { const messages: string[] = []; for (const key in errorObject.data) { const value = errorObject.data[key]; if (typeof value === "object" && value !== null) { // Always recursively process the value to extract all messages messages.push(...flattenErrors(value)); } } if (messages.length > 0) { return messages; } } // Process all object values recursively return Object.values(errorObject).flatMap(flattenErrors); } else if (typeof errors === "string") { return [errors]; } else { return []; } } export function pocketbaseErrorMessage(errors: unknown): string { const messages = flattenErrors(errors); return messages.length > 0 ? messages.join("\n") : "No errors found"; }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mrwyndham/pocketbase-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server