BioClaw add-gmail
Add Gmail integration to BioClaw. Can be configured as a tool (agent reads/sends emails when triggered from WhatsApp) or as a full channel (emails can trigger the agent, schedule tasks, and receive replies). Guides through GCP OAuth setup and implements the integration.
git clone https://github.com/Runchuan-BU/BioClaw
T=$(mktemp -d) && git clone --depth=1 https://github.com/Runchuan-BU/BioClaw "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/add-gmail" ~/.claude/skills/runchuan-bu-bioclaw-add-gmail && rm -rf "$T"
.claude/skills/add-gmail/SKILL.mdAdd Gmail Integration
This skill adds Gmail capabilities to BioClaw. It can be configured in two modes:
- Tool Mode - Agent can read/send emails, but only when triggered from WhatsApp
- Channel Mode - Emails can trigger the agent, schedule tasks, and receive email replies
Initial Questions
Ask the user:
How do you want to use Gmail with BioClaw?
Option 1: Tool Mode
- Agent can read and send emails when you ask it to
- Triggered only from WhatsApp (e.g., "@Bio check my email" or "@Bio send an email to...")
- Simpler setup, no email polling
Option 2: Channel Mode
- Everything in Tool Mode, plus:
- Emails to a specific address/label trigger the agent
- Agent replies via email (not WhatsApp)
- Can schedule tasks via email
- Requires email polling infrastructure
Store their choice and proceed to the appropriate section.
Prerequisites (Both Modes)
1. Check Existing Gmail Setup
First, check if Gmail is already configured:
ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found"
If
credentials.json exists, skip to "Verify Gmail Access" below.
2. Create Gmail Config Directory
mkdir -p ~/.gmail-mcp
3. GCP Project Setup
USER ACTION REQUIRED
Tell the user:
I need you to set up Google Cloud OAuth credentials. I'll walk you through it:
- Open https://console.cloud.google.com in your browser
- Create a new project (or select existing) - click the project dropdown at the top
Wait for user confirmation, then continue:
- Now enable the Gmail API:
- In the left sidebar, go to APIs & Services → Library
- Search for "Gmail API"
- Click on it, then click Enable
Wait for user confirmation, then continue:
- Now create OAuth credentials:
- Go to APIs & Services → Credentials (in the left sidebar)
- Click + CREATE CREDENTIALS at the top
- Select OAuth client ID
- If prompted for consent screen, choose "External", fill in app name (e.g., "BioClaw"), your email, and save
- For Application type, select Desktop app
- Name it anything (e.g., "BioClaw Gmail")
- Click Create
Wait for user confirmation, then continue:
- Download the credentials:
- Click DOWNLOAD JSON on the popup (or find it in the credentials list and click the download icon)
- Save it as
gcp-oauth.keys.jsonWhere did you save the file? (Give me the full path, or just paste the file contents here)
If user provides a path, copy it:
cp "/path/user/provided/gcp-oauth.keys.json" ~/.gmail-mcp/gcp-oauth.keys.json
If user pastes the JSON content, write it directly:
cat > ~/.gmail-mcp/gcp-oauth.keys.json << 'EOF' {paste the JSON here} EOF
Verify the file is valid JSON:
cat ~/.gmail-mcp/gcp-oauth.keys.json | head -5
4. OAuth Authorization
USER ACTION REQUIRED
Tell the user:
I'm going to run the Gmail authorization. A browser window will open asking you to sign in to Google and grant access.
Important: If you see a warning that the app isn't verified, click "Advanced" then "Go to [app name] (unsafe)" - this is normal for personal OAuth apps.
Run the authorization:
npx -y @gongrzhe/server-gmail-autoauth-mcp auth
If that doesn't work (some versions don't have an auth subcommand), run it and let it prompt:
timeout 60 npx -y @gongrzhe/server-gmail-autoauth-mcp || true
Tell user:
Complete the authorization in your browser. The window should close automatically when done. Let me know when you've authorized.
5. Verify Gmail Access
Check that credentials were saved:
if [ -f ~/.gmail-mcp/credentials.json ]; then echo "Gmail authorization successful!" ls -la ~/.gmail-mcp/ else echo "ERROR: credentials.json not found - authorization may have failed" fi
Test the connection by listing labels (quick sanity check):
echo '{"method": "tools/list"}' | timeout 10 npx -y @gongrzhe/server-gmail-autoauth-mcp 2>/dev/null | head -20 || echo "MCP responded (check output above)"
If everything works, proceed to implementation.
Tool Mode Implementation
For Tool Mode, integrate Gmail MCP into the agent runner. Execute these changes directly.
Step 1: Add Gmail MCP to Agent Runner
Read
container/agent-runner/src/index.ts and find the mcpServers config in the query() call.
Add
gmail to the mcpServers object:
gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }
Find the
allowedTools array and add Gmail tools:
'mcp__gmail__*'
The result should look like:
mcpServers: { bioclaw: ipcMcp, gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] } }, allowedTools: [ 'Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'mcp__bioclaw__*', 'mcp__gmail__*' ],
Step 2: Mount Gmail Credentials in Container
Read
src/container-runner.ts and find the buildVolumeMounts function.
Add this mount block (after the
.claude mount is a good location):
// Gmail credentials directory const gmailDir = path.join(homeDir, '.gmail-mcp'); if (fs.existsSync(gmailDir)) { mounts.push({ hostPath: gmailDir, containerPath: '/home/node/.gmail-mcp', readonly: false // MCP may need to refresh tokens }); }
Step 3: Update Group Memory
Append to
groups/CLAUDE.md (the global memory file):
## Email (Gmail) You have access to Gmail via MCP tools: - `mcp__gmail__search_emails` - Search emails with query - `mcp__gmail__get_email` - Get full email content by ID - `mcp__gmail__send_email` - Send an email - `mcp__gmail__draft_email` - Create a draft - `mcp__gmail__list_labels` - List available labels Example: "Check my unread emails from today" or "Send an email to john@example.com about the meeting"
Also append the same section to
groups/main/CLAUDE.md.
Step 4: Rebuild and Restart
Run these commands:
cd container && ./build.sh
Wait for container build to complete, then:
cd .. && npm run build
Wait for TypeScript compilation, then restart the service:
launchctl kickstart -k gui/$(id -u)/com.bioclaw
Check that it started:
sleep 2 && launchctl list | grep bioclaw
Step 5: Test Gmail Integration
Tell the user:
Gmail integration is set up! Test it by sending this message in your WhatsApp main channel:
@Bio check my recent emailsOr:
@Bio list my Gmail labels
Watch the logs for any errors:
tail -f logs/bioclaw.log
Channel Mode Implementation
Channel Mode includes everything from Tool Mode, plus email polling and routing.
Additional Questions for Channel Mode
Ask the user:
How should the agent be triggered from email?
Option A: Specific Label
- Create a Gmail label (e.g., "BioClaw")
- Emails with this label trigger the agent
- You manually label emails or set up Gmail filters
Option B: Email Address Pattern
- Emails to a specific address pattern (e.g., andy+task@gmail.com)
- Uses Gmail's plus-addressing feature
Option C: Subject Prefix
- Emails with a subject starting with a keyword (e.g., "[Bio]")
- Anyone can trigger the agent by using the prefix
Also ask:
How should email conversations be grouped?
Option A: Per Email Thread
- Each email thread gets its own conversation context
- Agent remembers the thread history
Option B: Per Sender
- All emails from the same sender share context
- Agent remembers all interactions with that person
Option C: Single Context
- All emails share the main group context
- Like an additional input to the main channel
Store their choices for implementation.
Step 1: Complete Tool Mode First
Complete all Tool Mode steps above before continuing. Verify Gmail tools work by having the user test
@Bio check my recent emails.
Step 2: Add Email Polling Configuration
Read
src/types.ts and add this interface:
export interface EmailChannelConfig { enabled: boolean; triggerMode: 'label' | 'address' | 'subject'; triggerValue: string; // Label name, address pattern, or subject prefix contextMode: 'thread' | 'sender' | 'single'; pollIntervalMs: number; replyPrefix?: string; // Optional prefix for replies }
Read
src/config.ts and add this configuration (customize values based on user's earlier answers):
export const EMAIL_CHANNEL: EmailChannelConfig = { enabled: true, triggerMode: 'label', // or 'address' or 'subject' triggerValue: 'BioClaw', // the label name, address pattern, or prefix contextMode: 'thread', pollIntervalMs: 60000, // Check every minute replyPrefix: '[Bio] ' };
Step 3: Add Email State Tracking
Read
src/db.ts and add these functions for tracking processed emails:
// Track processed emails to avoid duplicates export function initEmailTable(): void { db.exec(` CREATE TABLE IF NOT EXISTS processed_emails ( message_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, sender TEXT NOT NULL, subject TEXT, processed_at TEXT NOT NULL, response_sent INTEGER DEFAULT 0 ) `); } export function isEmailProcessed(messageId: string): boolean { const row = db.prepare('SELECT 1 FROM processed_emails WHERE message_id = ?').get(messageId); return !!row; } export function markEmailProcessed(messageId: string, threadId: string, sender: string, subject: string): void { db.prepare(` INSERT OR REPLACE INTO processed_emails (message_id, thread_id, sender, subject, processed_at) VALUES (?, ?, ?, ?, ?) `).run(messageId, threadId, sender, subject, new Date().toISOString()); } export function markEmailResponded(messageId: string): void { db.prepare('UPDATE processed_emails SET response_sent = 1 WHERE message_id = ?').run(messageId); }
Also find the
initDatabase() function in src/db.ts and add a call to initEmailTable().
Step 4: Create Email Channel Module
Create a new file
src/email-channel.ts with this content:
import { EMAIL_CHANNEL } from './config.js'; import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js'; import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', options: { colorize: true } } }); interface EmailMessage { id: string; threadId: string; from: string; subject: string; body: string; date: string; } // Gmail MCP client functions (call via subprocess or import the MCP directly) // These would invoke the Gmail MCP tools export async function checkForNewEmails(): Promise<EmailMessage[]> { // Build query based on trigger mode let query: string; switch (EMAIL_CHANNEL.triggerMode) { case 'label': query = `label:${EMAIL_CHANNEL.triggerValue} is:unread`; break; case 'address': query = `to:${EMAIL_CHANNEL.triggerValue} is:unread`; break; case 'subject': query = `subject:${EMAIL_CHANNEL.triggerValue} is:unread`; break; } // This requires calling Gmail MCP's search_emails tool // Implementation depends on how you want to invoke MCP from Node // Option 1: Use @anthropic-ai/claude-agent-sdk with just gmail MCP // Option 2: Run npx gmail MCP as subprocess and parse output // Option 3: Import gmail-autoauth-mcp directly // Placeholder - implement based on preference return []; } export async function sendEmailReply( threadId: string, to: string, subject: string, body: string ): Promise<void> { // Call Gmail MCP's send_email tool with in_reply_to for threading // Prefix subject with replyPrefix if configured const replySubject = subject.startsWith('Re:') ? subject : `Re: ${subject}`; const prefixedBody = EMAIL_CHANNEL.replyPrefix ? `${EMAIL_CHANNEL.replyPrefix}${body}` : body; // Implementation: invoke Gmail MCP send_email } export function getContextKey(email: EmailMessage): string { switch (EMAIL_CHANNEL.contextMode) { case 'thread': return `email-thread-${email.threadId}`; case 'sender': return `email-sender-${email.from.toLowerCase()}`; case 'single': return 'email-main'; } }
Step 5: Add Email Polling to Main Loop
Read
src/index.ts and add the email polling infrastructure. First, add these imports at the top:
import { checkForNewEmails, sendEmailReply, getContextKey } from './email-channel.js'; import { EMAIL_CHANNEL } from './config.js'; import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js';
Then add the
startEmailLoop function:
async function startEmailLoop(): Promise<void> { if (!EMAIL_CHANNEL.enabled) { logger.info('Email channel disabled'); return; } logger.info(`Email channel running (trigger: ${EMAIL_CHANNEL.triggerMode}:${EMAIL_CHANNEL.triggerValue})`); while (true) { try { const emails = await checkForNewEmails(); for (const email of emails) { if (isEmailProcessed(email.id)) continue; logger.info({ from: email.from, subject: email.subject }, 'Processing email'); markEmailProcessed(email.id, email.threadId, email.from, email.subject); // Determine which group/context to use const contextKey = getContextKey(email); // Build prompt with email content const prompt = `<email> <from>${email.from}</from> <subject>${email.subject}</subject> <body>${email.body}</body> </email> Respond to this email. Your response will be sent as an email reply.`; // Run agent with email context // You'll need to create a registered group for email or use a special handler const response = await runEmailAgent(contextKey, prompt, email); if (response) { await sendEmailReply(email.threadId, email.from, email.subject, response); markEmailResponded(email.id); logger.info({ to: email.from }, 'Email reply sent'); } } } catch (err) { logger.error({ err }, 'Error in email loop'); } await new Promise(resolve => setTimeout(resolve, EMAIL_CHANNEL.pollIntervalMs)); } }
Then add
startEmailLoop() in the main() function, after startMessageLoop():
// In main(), after startMessageLoop(): startEmailLoop();
Step 6: Implement Email Agent Runner
Add this function to
src/index.ts (or create a separate src/email-agent.ts if preferred):
async function runEmailAgent( contextKey: string, prompt: string, email: EmailMessage ): Promise<string | null> { // Email uses either: // 1. A dedicated "email" group folder // 2. Or dynamic folders per thread/sender const groupFolder = EMAIL_CHANNEL.contextMode === 'single' ? 'main' // Use main group context : `email/${contextKey}`; // Isolated email context // Ensure folder exists const groupDir = path.join(GROUPS_DIR, groupFolder); fs.mkdirSync(groupDir, { recursive: true }); // Create minimal registered group for email const emailGroup: RegisteredGroup = { name: contextKey, folder: groupFolder, trigger: '', // No trigger for email added_at: new Date().toISOString() }; // Use existing runContainerAgent const output = await runContainerAgent(emailGroup, { prompt, sessionId: sessions[groupFolder], groupFolder, chatJid: `email:${email.from}`, // Use email: prefix for JID isMain: false, isScheduledTask: false }); if (output.newSessionId) { sessions[groupFolder] = output.newSessionId; setSession(groupFolder, output.newSessionId); } return output.status === 'success' ? output.result : null; }
Step 7: Update IPC for Email Responses (Optional)
If you want the agent to be able to send emails proactively from within a session, read
container/agent-runner/src/ipc-mcp.ts and add this tool:
// Add to the MCP tools { name: 'send_email_reply', description: 'Send an email reply in the current thread', inputSchema: { type: 'object', properties: { body: { type: 'string', description: 'Email body content' } }, required: ['body'] } }
Then add handling in
src/ipc.ts in the processTaskIpc function or create a new IPC handler for email actions.
Step 8: Create Email Group Memory
Create the email group directory and memory file:
mkdir -p groups/email
Write
groups/email/CLAUDE.md:
# Email Channel You are responding to emails. Your responses will be sent as email replies. ## Guidelines - Be professional and clear - Keep responses concise but complete - Use proper email formatting (greetings, sign-off) - If the email requires action you can't take, explain what the user should do ## Context Each email thread or sender (depending on configuration) has its own conversation history.
Step 9: Rebuild and Test
Rebuild the container (required since agent-runner changed):
cd container && ./build.sh
Wait for build to complete, then compile TypeScript:
cd .. && npm run build
Restart the service:
launchctl kickstart -k gui/$(id -u)/com.bioclaw
Verify it started and check for email channel startup message:
sleep 3 && tail -20 logs/bioclaw.log | grep -i email
Tell the user:
Email channel is now active! Test it by sending an email that matches your trigger:
- Label mode: Apply the "${triggerValue}" label to any email
- Address mode: Send an email to ${triggerValue}
- Subject mode: Send an email with subject starting with "${triggerValue}"
The agent should process it within a minute and send a reply.
Monitor for the test:
tail -f logs/bioclaw.log | grep -E "(email|Email)"
Troubleshooting
Gmail MCP not responding
# Test Gmail MCP directly npx -y @gongrzhe/server-gmail-autoauth-mcp
OAuth token expired
# Re-authorize rm ~/.gmail-mcp/credentials.json npx -y @gongrzhe/server-gmail-autoauth-mcp
Emails not being detected
- Check the trigger configuration matches your test email
- Verify the label exists (for label mode)
- Check
table for already-processed emailsprocessed_emails
Container can't access Gmail
- Verify
is mounted in container~/.gmail-mcp - Check container logs:
cat groups/main/logs/container-*.log | tail -50
Removing Gmail Integration
To remove Gmail entirely:
-
Remove from
:container/agent-runner/src/index.ts- Delete
fromgmailmcpServers - Remove
frommcp__gmail__*allowedTools
- Delete
-
Remove from
:src/container-runner.ts- Delete the
mount block~/.gmail-mcp
- Delete the
-
Remove from
(Channel Mode only):src/index.ts- Delete
callstartEmailLoop() - Delete email-related imports
- Delete
-
Delete
(if created)src/email-channel.ts -
Remove Gmail sections from
groups/*/CLAUDE.md -
Rebuild:
cd container && ./build.sh && cd .. npm run build launchctl kickstart -k gui/$(id -u)/com.bioclaw