diff --git a/docs.json b/docs.json
index 938d200..0e65d4c 100644
--- a/docs.json
+++ b/docs.json
@@ -34,6 +34,14 @@
"metorial-101-monitoring"
]
},
+ {
+ "group": "Sample Projects",
+ "pages": [
+ "sample-projects-code-review-bot",
+ "sample-projects-slack-standup-bot",
+ "sample-projects-interview-coordinator-bot"
+ ]
+ },
{
"group": "API",
"pages": [
diff --git a/sample-projects-code-review-bot.mdx b/sample-projects-code-review-bot.mdx
new file mode 100644
index 0000000..bcac15a
--- /dev/null
+++ b/sample-projects-code-review-bot.mdx
@@ -0,0 +1,835 @@
+---
+title: "Code Review Bot with GitHub"
+description: "Create an AI-powered bot that analyzes pull requests for security issues, code smells, and style violations"
+---
+
+## What You'll Build
+
+Build an AI-powered code review bot that:
+- Reads code changes locally from your git repository
+- Analyzes code for security vulnerabilities, code smells, and style issues
+- Posts detailed review comments to GitHub (both general feedback and line-specific suggestions)
+- Automatically requests changes for issues or approves clean PRs
+
+This tutorial demonstrates practical AI-driven code analysis using Metorial's GitHub integration.
+
+
+**What you'll learn:**
+- Deploying the GitHub MCP server
+- Setting up OAuth for GitHub repositories
+- Creating an AI agent that reviews code
+- Posting GitHub review comments programmatically
+
+**Before you begin:**
+- [Create a Metorial account](/metorial-101-introduction)
+- [Create API keys](/api-getting-started)
+- GitHub account with a repository and at least one PR
+- Anthropic API key (Claude Sonnet 4 or newer recommended)
+
+**Time to complete:** 10-15 minutes
+
+
+## Prerequisites
+
+Before building the code review bot, ensure you have:
+
+1. **Metorial setup**:
+ - Active Metorial account at [app.metorial.com](https://app.metorial.com)
+ - Project created in your organization
+ - Metorial API key (generate in Dashboard → Home → Connect to Metorial)
+
+2. **GitHub repository**:
+ - Local clone of your GitHub repository
+ - Repository with write access on GitHub
+ - At least one pull request for testing
+ - Admin access to authorize OAuth
+
+3. **AI provider**:
+ - Anthropic API key (Claude Sonnet 4 or newer recommended for code analysis)
+
+4. **Development environment**:
+ - Node.js 18+ (TypeScript) or Python 3.9+ installed
+ - Basic knowledge of async/await patterns
+
+## Architecture Overview
+
+The code review bot workflow:
+
+1. **Input**: User provides local repository path, branch names, and PR number
+2. **Fetch Code Changes**: Bot reads git diff locally from your repository
+3. **AI Analysis**: Claude analyzes the diff for:
+ - Security vulnerabilities (SQL injection, XSS, exposed secrets)
+ - Code smells (duplication, long functions, complexity)
+ - Style violations (naming, formatting, consistency)
+ - Best practices (error handling, documentation, testing)
+4. **Post Review**: Bot posts the review to GitHub via the GitHub MCP server:
+ - Overall summary comment
+ - Line-specific feedback on issues
+ - Review decision (approve or request changes)
+
+**Tools used**: Local git (read code changes) + AI Model (code analysis) + GitHub MCP Server (post reviews)
+
+## Step 1: Deploy GitHub MCP Server
+
+Deploy the GitHub MCP server from Metorial's catalog to enable your bot to interact with GitHub.
+
+
+
+ In the Metorial Dashboard, go to **Servers** and search for "GitHub".
+
+
+
+ Click the **GitHub** server, then click **Deploy Server** → **Server Deployment**.
+
+ Give your deployment a descriptive name like "Code Review Bot GitHub".
+
+
+
+ After deployment, copy your **Server Deployment ID** from the deployment page. You'll need this for OAuth setup and in your bot code.
+
+
+
+
+Save your GitHub deployment ID—you'll need it for OAuth setup (Step 2) and in your bot code (Step 3).
+
+
+## Step 2: Set Up OAuth Authentication
+
+Your code review bot needs permission to access your GitHub repositories.
+
+
+
+ Install the Metorial SDK and Anthropic:
+
+
+ ```bash TypeScript
+ npm install metorial @metorial/anthropic @anthropic-ai/sdk
+ ```
+
+ ```bash Python
+ pip install metorial anthropic
+ ```
+
+
+
+
+ Run this code to generate the GitHub OAuth URL:
+
+
+ ```typescript TypeScript
+ import { Metorial } from 'metorial';
+
+ const metorial = new Metorial({
+ apiKey: "YOUR-METORIAL-API-KEY"
+ });
+
+ async function setupGitHubOAuth() {
+ const githubOAuth = await metorial.oauth.sessions.create({
+ serverDeploymentId: 'YOUR-GITHUB-DEPLOYMENT-ID'
+ });
+
+ console.log('Authorize GitHub here:', githubOAuth.url);
+ console.log('OAuth Session ID:', githubOAuth.id);
+
+ // Wait for authorization
+ await metorial.oauth.waitForCompletion([githubOAuth]);
+ console.log('✓ GitHub authorized!');
+
+ // Save githubOAuth.id for future use
+ return githubOAuth.id;
+ }
+
+ setupGitHubOAuth();
+ ```
+
+ ```python Python
+ import asyncio
+ from metorial import Metorial
+
+ async def setup_github_oauth():
+ metorial = Metorial(api_key="YOUR-METORIAL-API-KEY")
+
+ github_oauth = metorial.oauth.sessions.create(
+ server_deployment_id="YOUR-GITHUB-DEPLOYMENT-ID"
+ )
+
+ print(f"Authorize GitHub here: {github_oauth.url}")
+ print(f"OAuth Session ID: {github_oauth.id}")
+
+ # Wait for authorization
+ await metorial.oauth.wait_for_completion([github_oauth])
+ print("✓ GitHub authorized!")
+
+ # Save github_oauth.id for future use
+ return github_oauth.id
+
+ asyncio.run(setup_github_oauth())
+ ```
+
+
+
+
+ 1. Open the printed OAuth URL in your browser
+ 2. Sign in to GitHub if needed
+ 3. Review and approve the permissions (the bot needs `repo` scope to read PRs and post comments)
+ 4. You'll be redirected to your callback URL (or see a confirmation page)
+
+
+
+ Save the OAuth session ID securely. You'll reuse it for all future bot operations without re-authorizing.
+
+ For production apps, store OAuth session IDs in your database per user/repository.
+
+
+
+
+**Required OAuth Scopes:**
+
+The GitHub MCP server requires the `repo` scope for posting reviews, which provides:
+- Write access to post review comments and line-specific feedback
+- Permission to approve PRs or request changes on your repositories
+
+**Note**: If you plan to implement webhook-based automation (mentioned in Production Considerations), you may need additional scopes like `admin:repo_hook`. The basic bot functionality shown in this tutorial only requires `repo`.
+
+The required scopes are automatically requested when you authorize via the OAuth URL.
+
+
+## Step 3: Build the Code Review Bot
+
+Create the main bot that analyzes pull requests and posts review comments.
+
+
+```typescript TypeScript
+import { Metorial } from 'metorial';
+import { metorialAnthropic } from '@metorial/anthropic';
+import Anthropic from '@anthropic-ai/sdk';
+import { execSync } from 'child_process';
+import * as path from 'path';
+import * as fs from 'fs';
+
+const metorial = new Metorial({
+ apiKey: "YOUR-METORIAL-API-KEY"
+});
+
+const anthropic = new Anthropic({
+ apiKey: "YOUR_ANTHROPIC_API_KEY"
+});
+
+// GitHub integration credentials
+const GITHUB_DEPLOYMENT_ID = "GITHUB_DEPLOYMENT_ID";
+const GITHUB_OAUTH_SESSION_ID = "GITHUB_OAUTH_SESSION_ID";
+
+async function reviewPullRequest(
+ repoPath: string,
+ owner: string,
+ repo: string,
+ branchName: string,
+ prNumber: number,
+ baseBranch: string = 'main'
+) {
+ console.log(`Starting review of PR #${prNumber} (${branchName})`);
+
+ // Validate repository path
+ if (!fs.existsSync(repoPath)) {
+ throw new Error(`Repository path does not exist: ${repoPath}`);
+ }
+
+ const gitDir = path.join(repoPath, '.git');
+ if (!fs.existsSync(gitDir)) {
+ throw new Error(`Not a git repository: ${repoPath}`);
+ }
+
+ // Get diff from local repository
+ console.log(`Fetching diff: ${baseBranch}...${branchName}`);
+ const gitDiffCmd = `cd "${repoPath}" && git diff ${baseBranch}...${branchName}`;
+
+ let diffContent: string;
+ try {
+ diffContent = execSync(gitDiffCmd, { encoding: 'utf-8' });
+ } catch (error) {
+ throw new Error(`Git diff failed: ${error}`);
+ }
+
+ if (!diffContent.trim()) {
+ console.log('No changes found in diff');
+ return;
+ }
+
+ console.log(`Analyzing ${diffContent.length} characters of diff`);
+
+ // Create Metorial session with GitHub integration
+ await metorial.withProviderSession(
+ metorialAnthropic,
+ {
+ serverDeployments: [
+ {
+ serverDeploymentId: GITHUB_DEPLOYMENT_ID,
+ oauthSessionId: GITHUB_OAUTH_SESSION_ID
+ }
+ ],
+ streaming: false
+ },
+ async ({ tools, callTools }) => {
+ // Prepare review prompt with diff content
+ const messages: Anthropic.MessageParam[] = [
+ {
+ role: 'user',
+ content: `You are an expert code reviewer. You MUST post a review to GitHub after analyzing the code.
+
+Repository: ${owner}/${repo}
+Branch: ${branchName}
+PR Number: #${prNumber}
+
+DIFF CONTENT:
+\`\`\`diff
+${diffContent}
+\`\`\`
+
+CRITICAL: You MUST call the create_pull_request_review tool. Do NOT just provide analysis - you must POST the review to GitHub.
+
+INSTRUCTIONS:
+1. Analyze the diff for:
+ - Security vulnerabilities (SQL injection, XSS, exposed secrets, unsafe operations)
+ - Code quality issues (duplication, complexity, error handling)
+ - Style problems (naming, formatting, consistency)
+ - Best practices (documentation, testing, edge cases)
+
+2. IMMEDIATELY call create_pull_request_review tool with:
+ - owner: "${owner}"
+ - repo: "${repo}"
+ - pull_number: ${prNumber}
+ - body: Your summary of findings
+ - event: "APPROVE" (if no issues) or "REQUEST_CHANGES" (if issues found)
+ - comments: Array of line-specific comments if you found issues
+
+You MUST use the tool to post the review. START NOW.`
+ }
+ ];
+
+ // Send initial request to Claude
+ let response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 8192,
+ tools,
+ messages
+ });
+
+ // Handle tool calls in agentic loop
+ while (response.stop_reason === 'tool_use') {
+ const toolUseBlocks = response.content.filter(
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
+ );
+
+ console.log(`Executing ${toolUseBlocks.length} tool(s)...`);
+
+ // Execute tools via Metorial
+ const toolResults = await callTools(toolUseBlocks);
+
+ // Add assistant response and tool results to conversation
+ messages.push({ role: 'assistant', content: response.content });
+ messages.push(toolResults);
+
+ // Continue conversation
+ response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 8192,
+ messages,
+ tools
+ });
+ }
+
+ // Get final summary
+ const finalText = response.content
+ .filter((block): block is Anthropic.TextBlock => block.type === 'text')
+ .map(block => block.text)
+ .join('\n');
+
+ console.log(`Review completed: ${finalText}`);
+ }
+ );
+}
+```
+
+```python Python
+import asyncio
+import subprocess
+from pathlib import Path
+from metorial import Metorial
+from anthropic import AsyncAnthropic
+
+# Initialize clients
+metorial = Metorial(api_key="YOUR_METORIAL_API_KEY")
+anthropic = AsyncAnthropic(api_key="YOUR_ANTHROPIC_API_KEY")
+
+# GitHub integration credentials
+GITHUB_DEPLOYMENT_ID = "GITHUB_DEPLOYMENT_ID"
+GITHUB_OAUTH_SESSION_ID = "GITHUB_OAUTH_SESSION_ID"
+
+async def review_pull_request(
+ repo_path: str,
+ owner: str,
+ repo: str,
+ branch_name: str,
+ pr_number: int,
+ base_branch: str = "main"
+):
+ """
+ Review a pull request by analyzing local git diff and posting to GitHub.
+
+ Args:
+ repo_path: Local path to the git repository
+ owner: GitHub repository owner
+ repo: GitHub repository name
+ branch_name: Branch with changes to review
+ pr_number: GitHub PR number
+ base_branch: Base branch to compare against (default: "main")
+ """
+ print(f"Starting review of PR #{pr_number} ({branch_name})")
+
+ # Validate repository path
+ repo_dir = Path(repo_path)
+ if not repo_dir.exists():
+ raise ValueError(f"Repository path does not exist: {repo_path}")
+
+ git_dir = repo_dir / ".git"
+ if not git_dir.exists():
+ raise ValueError(f"Not a git repository: {repo_path}")
+
+ # Get diff from local repository
+ print(f"Fetching diff: {base_branch}...{branch_name}")
+ git_diff_cmd = f"cd '{repo_path}' && git diff {base_branch}...{branch_name}"
+ diff_result = subprocess.run(git_diff_cmd, shell=True, capture_output=True, text=True)
+
+ if diff_result.returncode != 0:
+ raise RuntimeError(f"Git diff failed: {diff_result.stderr}")
+
+ diff_content = diff_result.stdout
+ if not diff_content.strip():
+ print("No changes found in diff")
+ return
+
+ print(f"Analyzing {len(diff_content)} characters of diff")
+
+ # Create Metorial session with GitHub integration
+ async with metorial.provider_session(
+ provider="anthropic",
+ server_deployments=[
+ {
+ "serverDeploymentId": GITHUB_DEPLOYMENT_ID,
+ "oauthSessionId": GITHUB_OAUTH_SESSION_ID
+ }
+ ],
+ ) as session:
+ # Prepare review prompt with diff content
+ messages = [
+ {
+ "role": "user",
+ "content": f"""You are an expert code reviewer. You MUST post a review to GitHub after analyzing the code.
+
+Repository: {owner}/{repo}
+Branch: {branch_name}
+PR Number: #{pr_number}
+
+DIFF CONTENT:
+{diff_content}
+
+CRITICAL: You MUST call the create_pull_request_review tool. Do NOT just provide analysis - you must POST the review to GitHub.
+
+INSTRUCTIONS:
+1. Analyze the diff for:
+ - Security vulnerabilities (SQL injection, XSS, exposed secrets, unsafe operations)
+ - Code quality issues (duplication, complexity, error handling)
+ - Style problems (naming, formatting, consistency)
+ - Best practices (documentation, testing, edge cases)
+
+2. IMMEDIATELY call create_pull_request_review tool with:
+ - owner: "{owner}"
+ - repo: "{repo}"
+ - pull_number: {pr_number}
+ - body: Your summary of findings
+ - event: "APPROVE" (if no issues) or "REQUEST_CHANGES" (if issues found)
+ - comments: Array of line-specific comments if you found issues
+
+You MUST use the tool to post the review. START NOW."""
+ }
+ ]
+
+ # Send initial request to Claude
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8192,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Handle tool calls in agentic loop
+ while response.stop_reason == "tool_use":
+ # Extract tool use blocks from response
+ tool_use_blocks = [
+ block for block in response.content if block.type == "tool_use"
+ ]
+
+ print(f"Executing {len(tool_use_blocks)} tool(s)...")
+
+ # Execute tools via Metorial
+ tool_results = await session.call_tools(tool_use_blocks)
+
+ # Add assistant response to conversation
+ messages.append({"role": "assistant", "content": response.content})
+
+ # Format and add tool results to conversation
+ formatted_results = []
+ for tool_block, result in zip(tool_use_blocks, tool_results):
+ # Extract content from MCP-style or simple dict results
+ if isinstance(result, dict) and "contents" in result:
+ content = result["contents"][0]["text"] if result["contents"] else str(result)
+ elif isinstance(result, dict) and "content" in result:
+ content = result["content"]
+ else:
+ content = str(result)
+
+ formatted_results.append({
+ "type": "tool_result",
+ "tool_use_id": tool_block.id,
+ "content": content
+ })
+
+ messages.append({"role": "user", "content": formatted_results})
+
+ # Continue conversation
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8192,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Extract final response
+ final_text = "\n".join(
+ block.text for block in response.content if block.type == "text"
+ )
+
+ print(f"Review completed: {final_text}")
+```
+
+
+**What this code does:**
+
+1. **Reads git diff locally** from your repository between the base and feature branches
+2. **Creates a provider session** with GitHub MCP server for posting reviews
+3. **Sends the diff to Claude** with analysis instructions
+4. **AI analyzes the code** and identifies issues
+5. **AI posts review to GitHub** using `create_pull_request_review` tool with findings
+6. **Handles multi-step workflow** through agentic loop until review is complete
+7. **Handles errors gracefully**: If tool calls fail, the AI receives error messages and can retry or adjust its approach
+
+
+This uses **Claude's agentic capabilities**—the AI decides which tools to call and when. You don't need to write explicit logic for fetching files, analyzing code, or posting comments.
+
+
+## Step 4: Test with a Security Issue
+
+Let's test the bot with a PR containing a security vulnerability.
+
+**Scenario**: Create a test PR with SQL injection vulnerability.
+
+**Test PR content** (example):
+
+```javascript
+function getUserData(userId) {
+ const query = `SELECT * FROM users WHERE id = ${userId}`;
+ return database.query(query);
+}
+```
+
+**Run the bot**:
+
+
+```typescript TypeScript
+reviewPullRequest(
+ '/path/to/your/local/repo',
+ 'your-username',
+ 'your-repo',
+ 'feature-branch',
+ 123
+);
+```
+
+```python Python
+asyncio.run(review_pull_request(
+ repo_path="/path/to/your/local/repo",
+ owner="your-username",
+ repo="your-repo",
+ branch_name="feature-branch",
+ pr_number=123
+))
+```
+
+
+**Expected behavior**:
+1. Bot reads git diff from local repository for PR #123
+2. AI detects SQL injection vulnerability in the query string
+3. Bot posts review with:
+ - **General comment**: "Found 1 security vulnerability that needs immediate attention."
+ - **Line-specific comment** on the SQL query line: "🚨 SQL injection vulnerability detected. User input is directly interpolated into the query. Use parameterized queries instead: `SELECT * FROM users WHERE id = ?` with bound parameters."
+4. Bot submits review with **REQUEST_CHANGES** status
+
+
+The bot workflow:
+1. Reads git diff from your local repository
+2. Sends diff content to AI for analysis
+3. AI identifies the security issue in the code
+4. AI calls `create_pull_request_review` tool to post review to GitHub with `REQUEST_CHANGES` status and detailed comments
+
+The AI autonomously analyzes code and posts reviews—no manual orchestration needed!
+
+
+## Step 5: Test with Clean Code
+
+Test the bot with a clean PR to verify the approval workflow.
+
+**Scenario**: PR with well-written code.
+
+**Test PR content** (example):
+
+```typescript
+export function isValidEmail(email: string): boolean {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+}
+
+export function sanitizeInput(input: string): string {
+ return input
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+```
+
+**Run the bot**:
+
+
+```typescript TypeScript
+reviewPullRequest(
+ '/path/to/your/local/repo',
+ 'your-username',
+ 'your-repo',
+ 'feature-branch',
+ 124
+);
+```
+
+```python Python
+asyncio.run(review_pull_request(
+ repo_path="/path/to/your/local/repo",
+ owner="your-username",
+ repo="your-repo",
+ branch_name="feature-branch",
+ pr_number=124
+))
+```
+
+
+**Expected behavior**:
+1. Bot reads git diff from local repository for PR #124
+2. AI finds:
+ - ✓ Proper documentation with JSDoc comments
+ - ✓ Clear function names following conventions
+ - ✓ Security-conscious implementation (XSS prevention)
+ - ✓ No code smells or style violations
+3. Bot posts review comment: "Code looks excellent! Clean implementation with proper documentation, security considerations, and clear naming. The XSS sanitization is thorough and the email validation regex is appropriate."
+4. Bot submits review with **APPROVE** status
+
+## Troubleshooting
+
+Common issues and solutions when building your code review bot:
+
+
+
+ **Possible causes**:
+ - OAuth session expired or invalid
+ - PR doesn't exist or has no changed files
+ - AI model didn't call the review tools
+
+ **Solutions**:
+ 1. Verify your OAuth session is active: re-run the OAuth setup if needed
+ 2. Check the PR exists and has commits: `gh pr view `
+ 3. Increase `max_tokens` in the AI request (try 8192 instead of 4096)
+ 4. Review Metorial dashboard logs to see which tools were called
+ 5. Ensure your prompt explicitly instructs the AI to post reviews (see code examples)
+
+
+
+ **Possible causes**:
+ - Incorrect tool name in code
+ - GitHub MCP server deployment not active
+ - OAuth permissions insufficient
+
+ **Solutions**:
+ 1. Verify you're using the correct tool name: `create_pull_request_review`
+ 2. Check your GitHub server deployment is running in Metorial dashboard
+ 3. Confirm OAuth session is associated with the correct deployment ID
+ 4. Verify the `repo` scope was granted during OAuth authorization
+
+
+
+ **Possible causes**:
+ - AI prompt lacks specific guidelines
+ - Code context is truncated
+ - Model capabilities insufficient
+
+ **Solutions**:
+ 1. Add specific coding standards to your prompt (see "Custom Review Rules" in Advanced Customization)
+ 2. Increase `max_tokens` to allow longer analysis (8192-16384 for large PRs)
+ 3. Use Claude Sonnet 4 or newer for better code understanding
+ 4. Provide example issues in the prompt to guide the AI's analysis style
+
+
+
+ **Possible causes**:
+ - Callback URL mismatch
+ - Missing repository access
+ - GitHub App permissions not configured
+
+ **Solutions**:
+ 1. Verify your callback URL matches exactly (including http/https)
+ 2. Ensure you have admin access to the repository you're testing with
+ 3. Check the OAuth authorization screen shows the `repo` scope
+ 4. Try revoking and re-authorizing the OAuth connection
+ 5. Confirm your Metorial account and GitHub account are properly linked
+
+
+
+ **Possible causes**:
+ - Too many changed files
+ - AI context window exceeded
+ - API rate limits
+
+ **Solutions**:
+ 1. Filter files by extension or directory (modify the prompt to focus on specific file types)
+ 2. Implement batching: review files in chunks rather than all at once
+ 3. Set a maximum file size limit (skip files >1000 lines)
+ 4. Use streaming responses to handle longer processing times
+ 5. For PRs with >20 files, consider the performance optimizations in Production Considerations
+
+
+
+ **GitHub API limits**: 5,000 requests/hour for authenticated apps
+
+ **Solutions**:
+ 1. Implement exponential backoff when rate limit errors occur
+ 2. Cache PR data when running multiple reviews on the same PR
+ 3. Use conditional requests with ETags to avoid fetching unchanged data
+ 4. For production deployment, consider GitHub Enterprise with higher limits
+ 5. Track your usage in the Metorial dashboard to identify bottlenecks
+
+ **Prevention**: Queue bot reviews rather than triggering all at once, especially during peak hours.
+
+
+
+
+If you encounter errors not covered here, check the Metorial dashboard logs (Monitoring section) to see detailed tool execution traces and error messages. You can also inspect the actual API requests being made.
+
+
+## Advanced Customization
+
+Enhance your code review bot with these customizations:
+
+
+
+ Add company-specific coding standards to the AI prompt (e.g., "all public functions must have JSDoc comments", "use async/await instead of promises").
+
+
+
+ Customize prompts for different languages:
+ - Python: PEP 8 compliance, type hints
+ - TypeScript: strict mode, interface usage
+ - JavaScript: ESLint rules, modern syntax
+
+
+
+ Categorize issues as CRITICAL, WARNING, or SUGGESTION and adjust review status accordingly. Only block PRs for critical security issues.
+
+
+
+ Generate suggested code fixes for common issues. The AI can propose corrections in review comments (e.g., reformatted code, added error handling).
+
+
+
+**Example: Adding custom rules**
+
+Update the AI prompt with your standards:
+
+```typescript
+const customPrompt = `You are an expert code reviewer following these standards:
+
+Company Coding Standards:
+- All functions must have JSDoc comments
+- Use async/await instead of .then() promises
+- Maximum function length: 50 lines
+- All API responses must include error handling
+- Never use "any" type in TypeScript
+
+Review pull request #${prNumber}...`;
+```
+
+## Production Considerations
+
+Before deploying to production:
+
+1. **Webhook Integration**: Set up GitHub webhooks to trigger reviews automatically when PRs are opened or updated. You'll need the `admin:repo_hook` OAuth scope and a webhook endpoint that receives GitHub events. See GitHub's [Webhook documentation](https://docs.github.com/webhooks) for implementation details.
+2. **Rate Limiting**: Implement rate limiting to avoid hitting GitHub API limits (5000 requests/hour for authenticated apps)
+3. **Concurrency**: Queue reviews to handle multiple PRs simultaneously without overwhelming the AI API
+4. **Error Handling**: Add try/catch blocks and retry logic for API failures
+5. **Review History**: Store review results in a database for analytics and team insights
+6. **Configurable Rules**: Allow teams to customize review criteria per repository via config files
+7. **Cost Management**: Monitor AI API usage and token costs, especially for large PRs with many files
+8. **Privacy**: Ensure sensitive code doesn't get logged or sent to unauthorized services
+
+
+**Performance Tip:**
+
+For large PRs (>20 files), consider:
+- Reviewing only changed lines instead of full files
+- Batching file reviews to reduce token usage
+- Implementing a maximum file size limit
+- Allowing users to request specific file reviews
+
+
+## What's Next?
+
+Congratulations! You've built an AI-powered code review bot that analyzes pull requests for security issues, code quality, and best practices.
+
+### Learn More
+
+
+
+ Explore advanced SDK features and patterns.
+
+
+
+ Learn more about managing GitHub authorization.
+
+
+
+ Monitor bot performance and review activity.
+
+
+
+### Related Sample Projects
+
+
+
+ Build a bot that collects and summarizes team standup updates in Slack.
+
+
+
+ Create an AI coordinator that schedules interviews and sends professional emails.
+
+
+
+
+Need help? Email us at [support@metorial.com](mailto:support@metorial.com).
+
diff --git a/sample-projects-interview-coordinator-bot.mdx b/sample-projects-interview-coordinator-bot.mdx
new file mode 100644
index 0000000..e86fc04
--- /dev/null
+++ b/sample-projects-interview-coordinator-bot.mdx
@@ -0,0 +1,891 @@
+---
+title: "HR Interview Coordinator"
+description: "Learn how to build an AI agent that schedules interviews, sends professional emails, and coordinates with your team using Google Calendar and Gmail"
+---
+
+
+**What You'll Build**: An intelligent interview coordinator that automatically schedules interviews, sends confirmation emails to candidates, notifies your interview panel, and handles all follow-up communications.
+
+
+## Introduction
+
+Coordinating interviews involves juggling multiple calendars, sending professional emails, and ensuring everyone has the right information at the right time. This tutorial shows you how to build an AI-powered interview coordinator that handles all of this automatically.
+
+The bot will:
+- Check calendar availability (using the authenticated user's calendar)
+- Create calendar events with multiple attendees
+- Send professional confirmation emails to candidates
+- Notify interview panel members with prep materials
+- Request feedback from interviewers after completion
+
+By the end of this tutorial, you'll have a working interview coordinator that can handle the entire scheduling and communication workflow with natural language commands.
+
+
+**What You'll Learn**:
+- Deploying Google Calendar and Gmail MCP servers
+- Setting up OAuth for multiple Google services
+- Creating an AI agent that coordinates between multiple tools
+- Sending professional, AI-generated emails
+- Managing complex multi-step workflows with Claude
+
+
+## Prerequisites
+
+Before starting, make sure you have:
+
+- **Metorial Account**: [Sign up](https://app.metorial.com) and get your API key
+- **Google Workspace Account**: With Calendar and Gmail access
+- **Anthropic API Key**: Get one from [Anthropic Console](https://console.anthropic.com)
+- **Development Environment**: Node.js 18+ or Python 3.9+
+
+## Architecture Overview
+
+Here's how the interview coordinator works:
+
+1. **User provides interview details** (candidate, role, date preferences, interviewers)
+2. **AI checks calendar availability** using Google Calendar MCP server (checks the authenticated user's calendar)
+3. **AI creates calendar event** and adds interviewers as attendees
+4. **AI sends confirmation email** to candidate via Gmail
+5. **AI sends prep email** to interview panel via Gmail
+
+**Tools used**: Google Calendar MCP Server (scheduling) + Gmail MCP Server (communications) + AI Model (coordination)
+
+The AI autonomously decides which tools to use and in what order, handling complex multi-step workflows without explicit programming.
+
+## Step 1: Deploy Google Calendar MCP Server
+
+First, deploy the Google Calendar server from the Metorial catalog:
+
+
+
+ Go to [app.metorial.com/servers/catalog](https://app.metorial.com/servers/catalog)
+
+
+
+ Search for "Google Calendar" in the catalog
+
+
+
+ Click **Deploy** and note your deployment ID (starts with `svd_`)
+
+
+
+
+Save your Calendar deployment ID - you'll need it in Step 3. It looks like: `svd_abc123def456`
+
+**Note on Multiple Interviewers**: This deployment accesses **one** Google Calendar (the authenticated user's calendar). To check availability across multiple interviewers' calendars, each interviewer would need their own Calendar MCP deployment and OAuth session. For simplicity, this tutorial uses a single calendar instance.
+
+
+## Step 1b: Deploy Gmail MCP Server
+
+Next, deploy the Gmail server for email communications:
+
+
+
+ In the same catalog, search for "Gmail"
+
+
+
+ Click **Deploy** and note your Gmail deployment ID
+
+
+
+
+You now have two MCP servers deployed. Both will run in the same session, allowing the AI to coordinate between scheduling and email tools seamlessly.
+
+
+## Step 2: Set Up OAuth Authentication
+
+Both Calendar and Gmail require OAuth authentication to access your Google Workspace.
+
+
+
+
+ ```bash TypeScript
+ npm install metorial @metorial/anthropic @anthropic-ai/sdk
+ ```
+
+ ```bash Python
+ pip install metorial anthropic
+ ```
+
+
+
+
+
+ ```typescript TypeScript
+ import { Metorial } from "metorial";
+
+ const metorial = new Metorial({
+ apiKey: "YOUR_METORIAL_API_KEY",
+ });
+
+ async function setupOAuth() {
+ // Calendar OAuth
+ const calendarOAuth = await metorial.oauth.sessions.create({
+ serverDeploymentId: "YOUR_CALENDAR_DEPLOYMENT_ID",
+ });
+
+ console.log("Authorize Calendar here:", calendarOAuth.url);
+ console.log("Calendar OAuth Session ID:", calendarOAuth.id);
+
+ // Wait for authorization
+ await metorial.oauth.waitForCompletion([calendarOAuth]);
+ console.log("✓ Calendar authorized!");
+
+ // Gmail OAuth
+ const gmailOAuth = await metorial.oauth.sessions.create({
+ serverDeploymentId: "YOUR_GMAIL_DEPLOYMENT_ID",
+ });
+
+ console.log("\nAuthorize Gmail here:", gmailOAuth.url);
+ console.log("Gmail OAuth Session ID:", gmailOAuth.id);
+
+ // Wait for authorization
+ await metorial.oauth.waitForCompletion([gmailOAuth]);
+ console.log("✓ Gmail authorized!");
+
+ console.log("\nSave these OAuth Session IDs for your bot:");
+ console.log("Calendar:", calendarOAuth.id);
+ console.log("Gmail:", gmailOAuth.id);
+ }
+
+ setupOAuth();
+ ```
+
+ ```python Python
+ from metorial import Metorial
+
+ metorial = Metorial(api_key="YOUR_METORIAL_API_KEY")
+
+ # Calendar OAuth
+ calendar_oauth = metorial.oauth.sessions.create(
+ server_deployment_id="YOUR_CALENDAR_DEPLOYMENT_ID"
+ )
+
+ print("Authorize Calendar here:", calendar_oauth.url)
+ print("Calendar OAuth Session ID:", calendar_oauth.id)
+
+ # Wait for authorization
+ metorial.oauth.wait_for_completion([calendar_oauth])
+ print("✓ Calendar authorized!")
+
+ # Gmail OAuth
+ gmail_oauth = metorial.oauth.sessions.create(
+ server_deployment_id="YOUR_GMAIL_DEPLOYMENT_ID"
+ )
+
+ print("\nAuthorize Gmail here:", gmail_oauth.url)
+ print("Gmail OAuth Session ID:", gmail_oauth.id)
+
+ # Wait for authorization
+ metorial.oauth.wait_for_completion([gmail_oauth])
+ print("✓ Gmail authorized!")
+
+ print("\nSave these OAuth Session IDs for your bot:")
+ print("Calendar:", calendar_oauth.id)
+ print("Gmail:", gmail_oauth.id)
+ ```
+
+
+
+
+ Visit both URLs in your browser and complete the Google OAuth flow for Calendar and Gmail access. The script will wait for you to authorize before continuing.
+
+
+
+ The script outputs your OAuth session IDs (starting with `soas_`). Save these for use in Step 3.
+
+
+
+
+**OAuth Scopes**: Calendar needs `calendar.events` and `calendar.readonly`. Gmail needs `gmail.send` and `gmail.readonly`. These are configured automatically by Metorial.
+
+
+## Step 3: Build the Interview Coordinator
+
+Now let's build the main coordinator that orchestrates interview scheduling and communications.
+
+
+```typescript TypeScript
+import { Metorial } from "metorial";
+import { metorialAnthropic } from "@metorial/anthropic";
+import Anthropic from "@anthropic-ai/sdk";
+
+// Initialize clients
+const metorial = new Metorial({
+ apiKey: "YOUR_METORIAL_API_KEY",
+});
+
+const anthropic = new Anthropic({
+ apiKey: "YOUR_ANTHROPIC_API_KEY",
+});
+
+// Server deployment credentials
+const CALENDAR_DEPLOYMENT_ID = "CALENDAR_DEPLOYMENT_ID";
+const CALENDAR_OAUTH_SESSION_ID = "CALENDAR_OAUTH_SESSION_ID";
+const GMAIL_DEPLOYMENT_ID = "GMAIL_DEPLOYMENT_ID";
+const GMAIL_OAUTH_SESSION_ID = "GMAIL_OAUTH_SESSION_ID";
+
+interface InterviewRequest {
+ candidateName: string;
+ candidateEmail: string;
+ role: string;
+ interviewers: string[]; // Email addresses
+ durationMinutes: number;
+ preferredDates: string[]; // ISO format dates
+ interviewType: "technical" | "behavioral" | "panel";
+}
+
+async function scheduleInterview(request: InterviewRequest) {
+ console.log(`Scheduling ${request.interviewType} interview for ${request.candidateName}...`);
+
+ await metorial.withProviderSession(
+ metorialAnthropic,
+ {
+ serverDeployments: [
+ {
+ serverDeploymentId: CALENDAR_DEPLOYMENT_ID,
+ oauthSessionId: CALENDAR_OAUTH_SESSION_ID,
+ },
+ {
+ serverDeploymentId: GMAIL_DEPLOYMENT_ID,
+ oauthSessionId: GMAIL_OAUTH_SESSION_ID,
+ },
+ ],
+ streaming: false,
+ },
+ async ({ tools, callTools }) => {
+ const messages: Anthropic.MessageParam[] = [
+ {
+ role: "user",
+ content: `Schedule interview and send confirmations:
+
+Candidate: ${request.candidateName} (${request.candidateEmail})
+Role: ${request.role}
+Interviewers: ${request.interviewers.join(", ")}
+Duration: ${request.durationMinutes} minutes
+Preferred dates: ${request.preferredDates.join(", ")}
+Type: ${request.interviewType}
+
+Steps to complete:
+1. Check calendar availability for all interviewers on preferred dates
+2. Create calendar event with:
+ - Title: "${request.role} Interview - ${request.candidateName}"
+ - Add all interviewers as attendees
+3. Send confirmation email to candidate (${request.candidateEmail}) with:
+ - Subject: "Interview Scheduled - ${request.role}"
+ - Professional email including: interview date/time, what to prepare, who they'll meet
+4. Send prep email to interviewers (${request.interviewers.join(", ")}) with:
+ - Subject: "Interview Prep - ${request.candidateName}"
+ - Candidate background, interview format, evaluation guidelines
+
+CRITICAL: Only use actual calendar data. If no slots available, inform user. DO NOT create events without checking availability first.`,
+ },
+ ];
+
+ let response = await anthropic.messages.create({
+ model: "claude-sonnet-4-20250514",
+ max_tokens: 8192,
+ tools: tools,
+ messages: messages,
+ });
+
+ // Agentic loop - let AI use tools autonomously
+ while (response.stop_reason === "tool_use") {
+ const toolUseBlocks = response.content.filter(
+ (block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
+ );
+
+ console.log(`AI using ${toolUseBlocks.length} tool(s)...`);
+
+ const toolResults = await callTools(toolUseBlocks);
+
+ messages.push({ role: "assistant", content: response.content });
+ messages.push(toolResults);
+
+ response = await anthropic.messages.create({
+ model: "claude-sonnet-4-20250514",
+ max_tokens: 8192,
+ tools: tools,
+ messages: messages,
+ });
+ }
+
+ // Extract final response
+ const finalText = response.content
+ .filter((block): block is Anthropic.TextBlock => block.type === "text")
+ .map((block) => block.text)
+ .join("\n");
+
+ console.log("Interview coordination complete:", finalText);
+ }
+ );
+}
+
+// Example usage
+scheduleInterview({
+ candidateName: "John Novak",
+ candidateEmail: "john.novak@sample.com",
+ role: "Senior Software Engineer",
+ interviewers: [
+ "user@yourCompany.com", // Hiring manager
+ ],
+ durationMinutes: 45,
+ preferredDates: [
+ "2026-02-7T14:00:00Z", // First preference
+ "2026-02-7T5:00:00Z", // Second preference
+ "2026-02-7T15:00:00Z", // Third preference
+ ],
+ interviewType: "technical",
+});
+```
+
+```python Python
+import asyncio
+from metorial import Metorial
+from anthropic import AsyncAnthropic
+from typing import List, Literal
+
+# Initialize clients
+metorial = Metorial(api_key="YOUR_METORIAL_API_KEY")
+anthropic = AsyncAnthropic(api_key="YOUR_ANTHROPIC_API_KEY")
+
+# Server deployment credentials
+CALENDAR_DEPLOYMENT_ID = "YOUR_CALENDAR_DEPLOYMENT_ID"
+CALENDAR_OAUTH_SESSION_ID = "YOUR_CALENDAR_OAUTH_SESSION_ID"
+GMAIL_DEPLOYMENT_ID = "YOUR_GMAIL_DEPLOYMENT_ID"
+GMAIL_OAUTH_SESSION_ID = "YOUR_GMAIL_OAUTH_SESSION_ID"
+
+async def schedule_interview(
+ candidate_name: str,
+ candidate_email: str,
+ role: str,
+ interviewers: List[str],
+ duration_minutes: int,
+ preferred_dates: List[str],
+ interview_type: Literal["technical", "behavioral", "panel"]
+):
+ print(f"Scheduling {interview_type} interview for {candidate_name}...")
+
+ async with metorial.provider_session(
+ provider="anthropic",
+ server_deployments=[
+ {
+ "serverDeploymentId": CALENDAR_DEPLOYMENT_ID,
+ "oauthSessionId": CALENDAR_OAUTH_SESSION_ID
+ },
+ {
+ "serverDeploymentId": GMAIL_DEPLOYMENT_ID,
+ "oauthSessionId": GMAIL_OAUTH_SESSION_ID
+ }
+ ],
+ ) as session:
+ messages = [
+ {
+ "role": "user",
+ "content": f"""Schedule interview and send confirmations:
+
+Candidate: {candidate_name} ({candidate_email})
+Role: {role}
+Interviewers: {", ".join(interviewers)}
+Duration: {duration_minutes} minutes
+Preferred dates: {", ".join(preferred_dates)}
+Type: {interview_type}
+
+Steps to complete:
+1. Check calendar availability for all interviewers on preferred dates
+2. Create calendar event with:
+ - Title: "{role} Interview - {candidate_name}"
+ - Add all interviewers as attendees
+3. Send confirmation email to candidate ({candidate_email}) with:
+ - Subject: "Interview Scheduled - {role}"
+ - Professional email including: interview date/time, what to prepare, who they'll meet
+4. Send prep email to interviewers ({", ".join(interviewers)}) with:
+ - Subject: "Interview Prep - {candidate_name}"
+ - Candidate background, interview format, evaluation guidelines
+
+CRITICAL: Only use actual calendar data. If no slots available, inform user. DO NOT create events without checking availability first."""
+ }
+ ]
+
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8192,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Agentic loop - let AI use tools autonomously
+ while response.stop_reason == "tool_use":
+ tool_use_blocks = [
+ block for block in response.content if block.type == "tool_use"
+ ]
+
+ print(f"AI using {len(tool_use_blocks)} tool(s)...")
+
+ tool_results = await session.call_tools(tool_use_blocks)
+
+ messages.append({"role": "assistant", "content": response.content})
+ messages.append(tool_results)
+
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8192,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Extract final response
+ final_text = "\n".join(
+ block.text for block in response.content if block.type == "text"
+ )
+
+ print(f"Interview coordination complete: {final_text}")
+
+# Example usage
+asyncio.run(schedule_interview(
+ candidate_name="John Novak",
+ candidate_email="john.novak@sample.com",
+ role="Senior Software Engineer",
+ interviewers=[
+ "user@yourCompany.com", # Hiring manager
+ ],
+ duration_minutes=45,
+ preferred_dates=[
+ "2026-02-7T14:00:00Z", # First preference
+ "2026-02-7T5:00:00Z", # Second preference
+ "2026-02-7T15:00:00Z", # Third preference
+ ],
+ interview_type="technical"
+))
+```
+
+
+
+**Key Implementation Details**:
+- **Single Session**: Both Calendar and Gmail servers run in one session, allowing the AI to seamlessly switch between scheduling and emailing tools
+- **Agentic Workflow**: The AI autonomously decides which tools to use and in what order - no explicit programming needed
+- **Anti-Hallucination**: The prompt explicitly instructs using only actual calendar data to prevent AI from making up availability
+- **Professional Communications**: AI generates context-appropriate emails for candidates and interviewers
+
+
+
+**Multiple Interviewer Calendars**: The current implementation uses a single Google Calendar MCP server instance, which only accesses **one** Google Calendar (the calendar of the authenticated user). To check availability across **multiple interviewers' calendars**, you would need to:
+- Deploy a separate Calendar MCP server instance for each interviewer
+- Set up OAuth for each interviewer's calendar
+- Pass multiple calendar server deployments to the session
+
+For now, this tutorial demonstrates scheduling with a single calendar owner who can view their own availability. The `interviewers` array is used for adding attendees to the calendar event and sending prep emails, but **not** for checking their individual calendar availability.
+
+
+## Email Templates
+
+The AI automatically generates professional emails tailored to each recipient. Here are examples of what the coordinator sends:
+
+
+**Subject**: Interview Scheduled - Senior Software Engineer
+
+**Body**:
+```
+Hi Sarah,
+
+Great news! We've scheduled your interview for the Senior Software Engineer position.
+
+Interview Details:
+- Date: Tuesday, February 10, 2026
+- Time: 2:00 PM - 3:00 PM EST
+- Duration: 60 minutes
+- Format: Technical Interview
+
+You'll be meeting with:
+- Alex Chen, Hiring Manager
+- Jordan Smith, Technical Lead
+
+What to Prepare:
+- Please have your development environment ready for a live coding session
+- We'll be discussing system design and your recent project experience
+- Feel free to ask questions about our team and tech stack
+
+If you need to reschedule, please let us know as soon as possible.
+
+Looking forward to speaking with you!
+
+Best regards,
+Hiring Team
+```
+
+
+
+**Subject**: Interview Prep - Sarah Johnson
+
+**Body**:
+```
+Hi Team,
+
+You have an upcoming interview scheduled:
+
+Candidate: Sarah Johnson
+Position: Senior Software Engineer
+Interview Type: Technical
+Date: Tuesday, February 10, 2026 at 2:00 PM EST
+Duration: 60 minutes
+
+Interview Format:
+- First 10 minutes: Introductions and company overview
+- Next 35 minutes: Technical assessment (live coding + system design)
+- Final 15 minutes: Candidate questions
+
+Evaluation Focus:
+- Problem-solving approach and code quality
+- System design thinking
+- Communication and collaboration style
+- Cultural fit and team dynamics
+
+Please review the candidate's background before the interview and come prepared with your assessment criteria.
+
+If you need to reschedule, please notify the team immediately.
+
+Thanks,
+Recruiting Team
+```
+
+
+## Step 4: Test the Coordinator
+
+Let's test the coordinator with a realistic scenario:
+
+
+```typescript TypeScript
+// Test scenario: Schedule technical interview
+await scheduleInterview({
+ candidateName: "Alex Rivera",
+ candidateEmail: "alex.rivera@example.com",
+ role: "Staff Frontend Engineer",
+ interviewers: [
+ "engineering.director@company.com",
+ "senior.engineer@company.com",
+ "product.manager@company.com",
+ ],
+ durationMinutes: 90, // Panel interview
+ preferredDates: [
+ "2026-02-15T13:00:00Z",
+ "2026-02-16T14:00:00Z",
+ "2026-02-17T10:00:00Z",
+ ],
+ interviewType: "panel",
+});
+```
+
+```python Python
+# Test scenario: Schedule technical interview
+asyncio.run(schedule_interview(
+ candidate_name="Alex Rivera",
+ candidate_email="alex.rivera@example.com",
+ role="Staff Frontend Engineer",
+ interviewers=[
+ "engineering.director@company.com",
+ "senior.engineer@company.com",
+ "product.manager@company.com",
+ ],
+ duration_minutes=90, # Panel interview
+ preferred_dates=[
+ "2026-02-15T13:00:00Z",
+ "2026-02-16T14:00:00Z",
+ "2026-02-17T10:00:00Z",
+ ],
+ interview_type="panel"
+))
+```
+
+
+**Expected Output**:
+```
+Scheduling panel interview for Alex Rivera...
+AI using 3 tool(s)...
+AI using 2 tool(s)...
+AI using 2 tool(s)...
+Interview coordination complete: Successfully scheduled panel interview for Alex Rivera on February 15, 2026 at 1:00 PM EST. Calendar event created. Confirmation email sent to candidate and prep email sent to all 3 interviewers (Engineering Director, Senior Engineer, Product Manager).
+```
+
+The coordinator will:
+1. Check availability on the authenticated user's calendar (not all 3 interviewers - see note below)
+2. Find the first available slot (Feb 15 at 1:00 PM)
+3. Create a 90-minute calendar event with all 3 interviewers as attendees
+4. Send confirmation email to Alex with interview details
+5. Send prep email to all interviewers with candidate info
+
+
+**Calendar Availability Limitation**: This example checks only the authenticated user's calendar availability. To check all 3 interviewers' calendars, you would need to deploy a separate Calendar MCP instance for each interviewer with their OAuth credentials. The interviewers array is used for adding attendees to the event and sending emails, not for checking their individual availability.
+
+
+## Troubleshooting
+
+
+**Possible causes**:
+- OAuth session expired or invalid
+- Calendar API not enabled in Google Workspace
+- Insufficient permissions (need `calendar.events` scope)
+- No availability found in provided date range
+
+**Solutions**:
+- Re-run OAuth setup to refresh credentials
+- Verify Calendar API is enabled at [Google Cloud Console](https://console.cloud.google.com)
+- Check OAuth session has correct scopes in Metorial dashboard
+- Expand preferred date range or reduce number of required attendees
+
+
+
+**Possible causes**:
+- OAuth session expired for Gmail
+- Gmail API not enabled
+- Insufficient permissions (need `gmail.send` scope)
+- Daily sending limit reached (500 emails/day for standard accounts)
+
+**Solutions**:
+- Re-run OAuth setup for Gmail specifically
+- Enable Gmail API in Google Cloud Console
+- Verify OAuth session includes `gmail.send` scope
+- Check your Gmail sending quota at Google Workspace Admin
+
+
+
+**Possible causes**:
+- Sending from unverified domain
+- Email content triggers spam filters
+- High volume of emails in short period
+
+**Solutions**:
+- Set up SPF and DKIM records for your sending domain
+- Use professional, well-formatted email templates
+- Add delays between bulk sends
+- Test email content with spam checking tools
+- Whitelist your sending address with recipients
+
+
+
+**Issue**: Calendar event created despite conflicts
+
+**Solution**: This is likely a prompt issue. Ensure your prompt includes:
+```
+CRITICAL: Only use actual calendar data. If no slots available, inform user.
+DO NOT create events without checking availability first.
+```
+
+The anti-hallucination instruction forces the AI to actually check calendars before proceeding.
+
+
+
+**Issue**: Extra or missing attendees
+
+**Solution**: Be explicit in your prompt about who should attend:
+```
+Add ALL these interviewers as attendees: {interviewers.join(", ")}
+Do not add anyone else as an attendee.
+```
+
+Also verify the email addresses are correct in your request object.
+
+
+
+**Issue**: Need to verify availability across multiple interviewers' calendars
+
+**Current Limitation**: A single Google Calendar MCP server instance can only access one calendar (the authenticated user's calendar).
+
+**Solution**: To check availability across multiple interviewers:
+1. Deploy a separate Calendar MCP server instance for each interviewer
+2. Set up individual OAuth sessions for each interviewer's calendar
+3. Pass all calendar deployments to `serverDeployments` array in your session
+4. Update your prompt to check all calendar instances before scheduling
+
+Example with multiple calendars:
+```typescript
+serverDeployments: [
+ { serverDeploymentId: CALENDAR_1_ID, oauthSessionId: OAUTH_1_ID },
+ { serverDeploymentId: CALENDAR_2_ID, oauthSessionId: OAUTH_2_ID },
+ { serverDeploymentId: GMAIL_DEPLOYMENT_ID, oauthSessionId: GMAIL_OAUTH_ID }
+]
+```
+
+For this tutorial, we use a single calendar and add interviewers as attendees without checking their availability.
+
+
+## Advanced Customization
+
+
+
+ Customize email templates based on role, interview type, or company culture:
+
+ ```typescript
+ const emailTemplates = {
+ technical: "Include coding prep tips",
+ behavioral: "Focus on company values",
+ panel: "List all interviewers with bios"
+ };
+ ```
+
+ Pass templates to your prompt for context-aware emails.
+
+
+
+ Coordinate complex interview pipelines:
+
+ ```typescript
+ async function scheduleInterviewPipeline(
+ candidate: Candidate,
+ stages: InterviewStage[]
+ ) {
+ for (const stage of stages) {
+ await scheduleInterview({
+ ...candidate,
+ ...stage
+ });
+ }
+ }
+ ```
+
+ Schedule phone screen, technical, and panel interviews in sequence.
+
+
+
+ Request and aggregate interviewer feedback:
+
+ ```typescript
+ async function collectFeedback(
+ interviewId: string,
+ interviewers: string[]
+ ) {
+ // Send feedback forms after interview
+ // AI analyzes responses for hiring decision
+ }
+ ```
+
+ Automate post-interview feedback workflow.
+
+
+
+ Respect interviewer working hours and preferences:
+
+ ```typescript
+ const preferences = {
+ "eng.dir@co.com": {
+ hours: "9-17",
+ timezone: "America/New_York"
+ }
+ };
+ ```
+
+ Include preferences in AI prompt for smarter scheduling.
+
+
+
+ Send emails in candidate's preferred language:
+
+ ```typescript
+ const request = {
+ ...interviewDetails,
+ candidateLanguage: "es" // Spanish
+ };
+ ```
+
+ AI automatically generates emails in requested language.
+
+
+
+ Check availability across multiple interviewers' calendars:
+
+ ```typescript
+ // Deploy separate Calendar MCP for each interviewer
+ const INTERVIEWER_CALENDARS = [
+ { deploymentId: "svd_eng_dir", oauthId: "soas_eng_dir" },
+ { deploymentId: "svd_senior_eng", oauthId: "soas_senior" }
+ ];
+ ```
+
+ Each interviewer needs their own Calendar MCP instance with OAuth.
+
+
+
+## Production Considerations
+
+Before deploying to production:
+
+### Error Handling
+Add comprehensive error handling for common failures:
+```typescript
+try {
+ await scheduleInterview(request);
+} catch (error) {
+ if (error.message.includes("OAuth")) {
+ // Refresh OAuth tokens
+ } else if (error.message.includes("quota")) {
+ // Handle rate limits
+ } else {
+ // Log and alert on-call team
+ }
+}
+```
+
+### Monitoring and Logging
+Track key metrics for reliability:
+- Interview scheduling success rate
+- Email delivery rate
+- Calendar availability check duration
+- AI tool call patterns
+
+Use Metorial's built-in monitoring to track these metrics.
+
+### Testing Strategy
+Test edge cases before production:
+- All interviewers unavailable on all dates
+- Invalid email addresses
+- Time zone edge cases (DST transitions)
+- Calendar quota limits
+- Gmail sending limits
+
+### Privacy and Compliance
+Ensure GDPR/CCPA compliance:
+- Get consent before sending emails
+- Don't store candidate data longer than necessary
+- Encrypt sensitive information in transit and at rest
+
+### Scalability
+For high-volume hiring:
+- Implement request queuing to avoid rate limits
+- Cache calendar availability queries
+- Batch similar operations
+- Use Metorial's webhook support for async processing
+
+## What's Next
+
+Now that you have a working interview coordinator, explore more capabilities:
+
+
+
+ Learn advanced SDK patterns and best practices
+
+
+
+ Deep dive into OAuth authentication flows
+
+
+
+ Set up alerts and dashboards for your coordinator
+
+
+
+ Explore more MCP servers to add capabilities
+
+
+
+## Related Sample Projects
+
+
+
+ Build an AI code reviewer that posts comments on pull requests
+
+
+
+ Create a bot that collects and summarizes team standup updates
+
+
diff --git a/sample-projects-slack-standup-bot.mdx b/sample-projects-slack-standup-bot.mdx
new file mode 100644
index 0000000..7dae0fd
--- /dev/null
+++ b/sample-projects-slack-standup-bot.mdx
@@ -0,0 +1,827 @@
+---
+title: "PM Slack Standup Bot"
+description: "Create an AI-powered bot that collects daily standup responses, analyzes team progress, and posts executive summaries"
+---
+
+## What You'll Build
+
+Build an AI-powered Slack standup bot that:
+- Posts daily standup prompts to your team channel
+- Collects responses in organized threads
+- AI analyzes progress, blockers, and team dependencies
+- Generates executive summaries with key insights
+- Posts summaries to leadership channels
+- Tracks recurring blockers and patterns over time
+
+This tutorial demonstrates practical AI-driven team communication using Metorial's Slack integration.
+
+
+**What you'll learn:**
+- Deploying the Slack MCP server
+- Setting up OAuth for Slack workspace access
+- Creating an AI agent that processes team updates
+- Posting and reading Slack messages programmatically
+
+**Before you begin:**
+- [Create a Metorial account](/metorial-101-introduction)
+- [Create API keys](/api-getting-started)
+- Slack workspace with admin access
+- Anthropic API key (Claude Sonnet 4 or newer recommended)
+
+**Time to complete:** 10-15 minutes
+
+
+## Prerequisites
+
+Before building the standup bot, ensure you have:
+
+1. **Metorial setup**:
+ - Active Metorial account at [app.metorial.com](https://app.metorial.com)
+ - Project created in your organization
+ - Metorial API key (generate in Dashboard → Home → Connect to Metorial)
+
+2. **Slack workspace**:
+ - Admin access to install apps
+ - Channel where standups will be posted
+ - Leadership/executive channel for summaries (optional)
+
+3. **AI provider**:
+ - Anthropic API key (Claude Sonnet 4 or newer recommended for analysis)
+
+4. **Development environment**:
+ - Node.js 18+ (TypeScript) or Python 3.9+ installed
+ - Basic knowledge of async/await patterns
+
+## Architecture Overview
+
+The standup bot workflow:
+
+1. **Trigger**: Bot posts standup prompt to team channel (scheduled or manual)
+2. **Collection**: Team members reply in thread with their updates
+3. **Analysis**: AI analyzes all responses for:
+ - Individual progress and accomplishments
+ - Blockers and dependencies between team members
+ - Team sentiment and morale
+ - Recurring issues
+4. **Summary**: Bot generates and posts executive summary to leadership channel
+5. **Storage**: Optionally stores historical data for trend analysis
+
+**Tools used**: Slack MCP Server (post messages, read threads) + AI Model (analysis and summarization)
+
+## Step 1: Deploy Slack MCP Server
+
+Deploy the Slack MCP server from Metorial's catalog to enable your bot to interact with Slack.
+
+
+
+ In the Metorial Dashboard, go to **Servers** and search for "Slack".
+
+
+
+ Click the **Slack** server, then click **Deploy Server** → **Server Deployment**.
+
+ Give your deployment a descriptive name like "Standup Bot Slack".
+
+
+
+ After deployment, copy your **Server Deployment ID** from the deployment page. You'll need this for OAuth setup and in your bot code.
+
+
+
+
+Save your Slack deployment ID—you'll need it for OAuth setup (Step 2) and in your bot code (Step 3).
+
+
+## Step 2: Set Up OAuth Authentication
+
+Your standup bot needs permission to post messages and read thread replies in your Slack workspace.
+
+
+
+ Install the Metorial SDK and Anthropic:
+
+
+ ```bash TypeScript
+ npm install metorial @metorial/anthropic @anthropic-ai/sdk
+ ```
+
+ ```bash Python
+ pip install metorial anthropic
+ ```
+
+
+
+
+ Run this code to generate the Slack OAuth URL:
+
+
+ ```typescript TypeScript
+ import { Metorial } from 'metorial';
+
+ const metorial = new Metorial({
+ apiKey: "YOUR-METORIAL-API-KEY"
+ });
+
+ async function setupSlackOAuth() {
+ const slackOAuth = await metorial.oauth.sessions.create({
+ serverDeploymentId: 'YOUR-SLACK-DEPLOYMENT-ID'
+ });
+
+ console.log('Authorize Slack here:', slackOAuth.url);
+ console.log('OAuth Session ID:', slackOAuth.id);
+
+ // Wait for authorization
+ await metorial.oauth.waitForCompletion([slackOAuth]);
+ console.log('✓ Slack authorized!');
+
+ // Save slackOAuth.id for future use
+ return slackOAuth.id;
+ }
+
+ setupSlackOAuth();
+ ```
+
+ ```python Python
+ import asyncio
+ from metorial import Metorial
+
+ async def setup_slack_oauth():
+ metorial = Metorial(api_key="YOUR-METORIAL-API-KEY")
+
+ slack_oauth = metorial.oauth.sessions.create(
+ server_deployment_id="YOUR-SLACK-DEPLOYMENT-ID"
+ )
+
+ print(f"Authorize Slack here: {slack_oauth.url}")
+ print(f"OAuth Session ID: {slack_oauth.id}")
+
+ # Wait for authorization
+ await metorial.oauth.wait_for_completion([slack_oauth])
+ print("✓ Slack authorized!")
+
+ # Save slack_oauth.id for future use
+ return slack_oauth.id
+
+ asyncio.run(setup_slack_oauth())
+ ```
+
+
+
+
+ 1. Open the printed OAuth URL in your browser
+ 2. Sign in to Slack if needed
+ 3. Review and approve the permissions (the bot needs to post messages and read channels)
+ 4. You'll be redirected to your callback URL (or see a confirmation page)
+
+
+
+ Save the OAuth session ID securely. You'll reuse it for all future bot operations without re-authorizing.
+
+ For production apps, store OAuth session IDs in your database or environment variables.
+
+
+
+
+**Required OAuth Scopes:**
+
+The Slack MCP server requires these scopes:
+- `chat:write` - Post messages to channels
+- `channels:read` - Read public channel information
+- `channels:history` - Read message history to collect thread replies
+- `users:read` - Get user information for mentions
+
+The required scopes are automatically requested when you authorize via the OAuth URL.
+
+
+## Step 3: Build the Standup Bot
+
+Create the main bot that collects standup responses and generates summaries.
+
+
+```typescript TypeScript
+import { Metorial } from 'metorial';
+import { metorialAnthropic } from '@metorial/anthropic';
+import Anthropic from '@anthropic-ai/sdk';
+
+const metorial = new Metorial({
+ apiKey: "YOUR-METORIAL-API-KEY"
+});
+
+const anthropic = new Anthropic({
+ apiKey: "YOUR_ANTHROPIC_API_KEY"
+});
+
+// Slack integration credentials
+const SLACK_DEPLOYMENT_ID = "YOUR_SLACK_DEPLOYMENT_ID";
+const SLACK_OAUTH_SESSION_ID = "YOUR_SLACK_OAUTH_SESSION_ID";
+
+async function runStandup(
+ channelId: string,
+ summaryChannelId: string,
+ collectionTimeMinutes: number = 5
+) {
+ console.log(`Starting standup in channel ${channelId}`);
+
+ // Post standup prompt
+ const standupPrompt = `Good morning team! Time for daily standup. Please reply to this thread with:
+
+1. What you accomplished yesterday
+2. What you're working on today
+3. Any blockers or help needed
+
+You have ${collectionTimeMinutes} minutes to respond.`;
+
+ await metorial.withProviderSession(
+ metorialAnthropic,
+ {
+ serverDeployments: [
+ {
+ serverDeploymentId: SLACK_DEPLOYMENT_ID,
+ oauthSessionId: SLACK_OAUTH_SESSION_ID
+ }
+ ],
+ streaming: false
+ },
+ async ({ tools, callTools }) => {
+ // Post the standup prompt
+ const messages: Anthropic.MessageParam[] = [
+ {
+ role: 'user',
+ content: `Post a message to Slack channel ${channelId} with this text:
+
+"${standupPrompt}"
+
+Use the chat_postMessage tool.`
+ }
+ ];
+
+ console.log('Posting standup prompt...');
+ let response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 4096,
+ tools,
+ messages
+ });
+
+ // Handle tool calls
+ while (response.stop_reason === 'tool_use') {
+ const toolUseBlocks = response.content.filter(
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
+ );
+
+ const toolResults = await callTools(toolUseBlocks);
+
+ messages.push({ role: 'assistant', content: response.content });
+ messages.push(toolResults);
+
+ response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 4096,
+ messages,
+ tools
+ });
+ }
+
+ console.log('Standup prompt posted successfully!');
+ console.log(`Waiting ${collectionTimeMinutes} minutes for responses...`);
+
+ // Wait for responses
+ await new Promise(resolve => setTimeout(resolve, collectionTimeMinutes * 60 * 1000));
+
+ console.log('Collecting and analyzing responses...');
+
+ messages.push({
+ role: 'user',
+ content: `Collect standup responses and post summary:
+
+1. List recent messages in channel ${channelId} to find the standup prompt
+2. Get thread replies for that message
+3. Analyze ONLY actual user responses (skip bot's prompt)
+4. Post executive summary to ${summaryChannelId}
+
+Summary format:
+- Team Progress: Actual accomplishments mentioned
+- Today's Focus: Actual plans shared
+- Blockers: Actual issues raised
+- Action Items: Specific help requested
+
+CRITICAL: Base summary ONLY on real messages. If no responses, say "No responses received." DO NOT invent content.`
+ });
+
+ response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 8192,
+ tools,
+ messages
+ });
+
+ // Agentic loop
+ while (response.stop_reason === 'tool_use') {
+ const toolUseBlocks = response.content.filter(
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
+ );
+
+ console.log(`Executing ${toolUseBlocks.length} tool(s)...`);
+
+ const toolResults = await callTools(toolUseBlocks);
+
+ messages.push({ role: 'assistant', content: response.content });
+ messages.push(toolResults);
+
+ response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 8192,
+ messages,
+ tools
+ });
+ }
+
+ // Get final summary
+ const finalText = response.content
+ .filter((block): block is Anthropic.TextBlock => block.type === 'text')
+ .map(block => block.text)
+ .join('\n');
+
+ console.log(`Standup complete: ${finalText}`);
+ }
+ );
+}
+
+// Example usage
+runStandup(
+ 'C1234567890', // Your team channel ID
+ 'C0987654321', // Your executive channel ID
+ 5 // Minutes to wait for responses
+);
+```
+
+```python Python
+import asyncio
+from metorial import Metorial
+from anthropic import AsyncAnthropic
+
+# Initialize clients
+metorial = Metorial(api_key="YOUR-METORIAL-API-KEY")
+anthropic = AsyncAnthropic(api_key="YOUR-ANTHROPIC-API-KEY")
+
+# Slack integration credentials
+SLACK_DEPLOYMENT_ID = "SLACK_DEPLOYMENT_ID"
+SLACK_OAUTH_SESSION_ID = "SLACK_OAUTH_SESSION_ID"
+
+async def run_standup(
+ channel_id: str,
+ summary_channel_id: str,
+ collection_time_minutes: int = 5
+):
+ print(f"Starting standup in channel {channel_id}")
+
+ # Post standup prompt
+ standup_prompt = f"""Good morning team! Time for daily standup. Please reply to this thread with:
+
+1. What you accomplished yesterday
+2. What you're working on today
+3. Any blockers or help needed
+
+You have {collection_time_minutes} minutes to respond."""
+
+ async with metorial.provider_session(
+ provider="anthropic",
+ server_deployments=[
+ {
+ "serverDeploymentId": SLACK_DEPLOYMENT_ID,
+ "oauthSessionId": SLACK_OAUTH_SESSION_ID
+ }
+ ],
+ ) as session:
+ # Post the standup prompt
+ messages = [
+ {
+ "role": "user",
+ "content": f"""Post a message to Slack channel {channel_id} with this text:
+
+"{standup_prompt}"
+
+Use the chat_postMessage tool."""
+ }
+ ]
+
+ print("Posting standup prompt...")
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=4096,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Handle tool calls
+ while response.stop_reason == "tool_use":
+ tool_use_blocks = [
+ block for block in response.content if block.type == "tool_use"
+ ]
+
+ tool_results = await session.call_tools(tool_use_blocks)
+
+ messages.append({"role": "assistant", "content": response.content})
+ messages.append(tool_results)
+
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=4096,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ print(f"Standup prompt posted successfully!")
+ print(f"Waiting {collection_time_minutes} minutes for responses...")
+
+ # Wait for responses
+ await asyncio.sleep(collection_time_minutes * 60)
+
+ print("Collecting and analyzing responses...")
+
+ messages.append({
+ "role": "user",
+ "content": f"""Collect standup responses and post summary:
+
+1. List recent messages in channel {channel_id} to find the standup prompt
+2. Get thread replies for that message
+3. Analyze ONLY actual user responses (skip bot's prompt)
+4. Post executive summary to {summary_channel_id}
+
+Summary format:
+- Team Progress: Actual accomplishments mentioned
+- Today's Focus: Actual plans shared
+- Blockers: Actual issues raised
+- Action Items: Specific help requested
+
+CRITICAL: Base summary ONLY on real messages. If no responses, say "No responses received." DO NOT invent content."""
+ })
+
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8192,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Agentic loop
+ while response.stop_reason == "tool_use":
+ tool_use_blocks = [
+ block for block in response.content if block.type == "tool_use"
+ ]
+
+ print(f"Executing {len(tool_use_blocks)} tool(s)...")
+
+ tool_results = await session.call_tools(tool_use_blocks)
+
+ messages.append({"role": "assistant", "content": response.content})
+ messages.append(tool_results)
+
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8192,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Get final summary
+ final_text = "\n".join(
+ block.text for block in response.content if block.type == "text"
+ )
+
+ print(f"Standup complete: {final_text}")
+
+# Example usage
+asyncio.run(run_standup(
+ channel_id="C1234567890", # Your team channel ID
+ summary_channel_id="C0987654321", # Your executive channel ID
+ collection_time_minutes=5 # Minutes to wait for responses
+))
+```
+
+
+**What this code does:**
+
+1. **Posts standup prompt** to the team channel with clear instructions
+2. **Waits for responses** (configurable time)
+3. **Collects all thread replies** using the Slack MCP server
+4. **AI analyzes responses** for progress, blockers, and dependencies
+5. **Generates executive summary** with key insights
+6. **Posts summary** to leadership channel
+7. **Uses agentic workflow** - AI decides which Slack tools to call and when
+
+
+This uses **Claude's agentic capabilities**—the AI decides when to read the thread, how to analyze the data, and when to post the summary. You don't need to write explicit logic for parsing responses or formatting summaries.
+
+
+## Step 4: Test the Bot
+
+Let's test the bot with example standup responses.
+
+**Scenario**: Run standup in a test channel with your team.
+
+**Example responses**:
+```
+@alice: Yesterday finished the user auth feature. Today working on password reset. No blockers.
+
+@bob: Completed API endpoints for /users. Today starting the /products endpoints. Blocked on database schema approval.
+
+@charlie: Fixed 5 bugs in the dashboard. Today continuing bug fixes. Could use help reviewing PR #234.
+```
+
+**Run the bot**:
+
+
+```typescript TypeScript
+runStandup(
+ 'C1234567890', // Team channel ID
+ 'C0987654321', // Executive channel ID
+ 5 // Wait 5 minutes
+);
+```
+
+```python Python
+asyncio.run(run_standup(
+ channel_id="C1234567890", # Your team channel ID
+ summary_channel_id="C0987654321", # Your executive channel ID
+ collection_time_minutes=5 # Minutes to wait for responses
+))
+```
+
+
+**Expected behavior**:
+1. Bot posts standup prompt to team channel
+2. Team members reply in thread
+3. After 5 minutes, bot collects all responses
+4. AI analyzes and generates summary:
+
+```
+📊 Daily Standup Summary - [Date]
+
+✅ Team Progress (Yesterday):
+• Alice: Completed user authentication feature
+• Bob: Finished API endpoints for /users module
+• Charlie: Resolved 5 dashboard bugs
+
+🎯 Today's Focus:
+• Alice: Password reset functionality
+• Bob: /products API endpoints
+• Charlie: Continue bug fixing
+
+🚧 Blockers & Dependencies:
+• Bob: Waiting on database schema approval (blocking /products work)
+• Charlie: Needs code review on PR #234
+
+📈 Team Health: Positive momentum, steady progress
+
+⚠️ Action Items:
+1. Expedite database schema approval for Bob
+2. Assign reviewer to PR #234 for Charlie
+```
+
+5. Summary is posted to executive channel
+
+## Troubleshooting
+
+Common issues and solutions when building your standup bot:
+
+
+
+ **Possible causes**:
+ - OAuth session expired or invalid
+ - Incorrect channel ID
+ - Bot doesn't have permission to post in channel
+
+ **Solutions**:
+ 1. Verify your OAuth session is active: re-run the OAuth setup if needed
+ 2. Check the channel ID is correct (not channel name)
+ 3. Ensure the bot is invited to the channel: `/invite @YourBot`
+ 4. Verify OAuth scopes include `chat:write`
+ 5. Check Metorial dashboard logs for API errors
+
+
+
+ **Possible causes**:
+ - Missing `channels:history` scope
+ - Incorrect thread timestamp
+ - Bot not in channel
+
+ **Solutions**:
+ 1. Verify OAuth scopes include `channels:history`
+ 2. Check the thread_ts value matches the original message
+ 3. Ensure bot is member of the channel
+ 4. Try in a public channel first (private channels need additional setup)
+
+
+
+ **Possible causes**:
+ - Prompt lacks specific instructions
+ - Not enough context provided
+ - Model running out of tokens
+
+ **Solutions**:
+ 1. Add more specific analysis guidelines to the prompt
+ 2. Increase `max_tokens` to 8192 or higher
+ 3. Use Claude Sonnet 4 or newer for better understanding
+ 4. Provide example summaries in the prompt
+ 5. Include team-specific context (project names, terminology)
+
+
+
+ **Possible causes**:
+ - Not a Slack admin
+ - Callback URL mismatch
+ - App not approved for workspace
+
+ **Solutions**:
+ 1. Ensure you have admin privileges in Slack workspace
+ 2. Verify callback URL matches in Metorial dashboard
+ 3. Check workspace settings allow app installations
+ 4. Try revoking and re-authorizing
+ 5. Contact your Slack workspace admin if permissions are restricted
+
+
+
+ **Possible causes**:
+ - Collection time too short
+ - Wrong thread timestamp
+ - API rate limiting
+
+ **Solutions**:
+ 1. Increase `collection_time_minutes` to give team more time
+ 2. Verify the thread_ts is correct
+ 3. Check Slack API rate limits (50+ requests per minute)
+ 4. Add error handling to retry failed API calls
+ 5. Log the thread_ts immediately after posting to debug
+
+
+
+ **Slack API limits**:
+ - Tier 1 methods: 1 request per minute
+ - Tier 2 methods: 20 requests per minute
+ - Tier 3 methods: 50 requests per minute
+
+ **Solutions**:
+ 1. Implement exponential backoff for rate limit errors
+ 2. Cache channel and user information
+ 3. Batch operations when possible
+ 4. Use Slack's rate limit headers to track usage
+ 5. For high-frequency bots, consider Slack's paid tiers
+
+
+
+
+If you encounter errors not covered here, check the Metorial dashboard logs (Monitoring section) to see detailed tool execution traces and Slack API responses.
+
+
+## Advanced Customization
+
+Enhance your standup bot with these customizations:
+
+
+
+ Customize standup questions for your team's needs (e.g., "What are you learning today?", "Team shoutouts", "Health check: 1-5").
+
+
+
+ Run standups across multiple teams with different channels and schedules. Store team configs in a database.
+
+
+
+ Store historical standup data to track:
+ - Recurring blockers
+ - Team velocity trends
+ - Common challenges
+ Generate weekly/monthly reports
+
+
+
+ Send DMs to team members who haven't responded. Use Slack's users.list to track participation rates.
+
+
+
+ Connect with project management tools (Linear, Jira) to:
+ - Link updates to specific tasks
+ - Auto-update task status
+ - Cross-reference blockers with tickets
+
+
+
+ Track team morale over time by analyzing sentiment in standup responses. Alert leadership to significant drops.
+
+
+
+**Example: Custom questions**
+
+Update the standup prompt:
+
+```typescript
+const standupPrompt = `Good morning team! 🌅 Daily standup time:
+
+1. 🎯 What are you working on today?
+2. 🚧 Any blockers?
+3. 💡 What are you learning?
+4. 🙌 Team shoutouts (optional)
+5. ❤️ Health check: 1-5 (1=struggling, 5=great)
+
+Reply in thread, you have ${collectionTimeMinutes} minutes.`;
+```
+
+## Production Considerations
+
+Before deploying to production:
+
+1. **Scheduling**: Set up daily automated runs:
+ - Use cron jobs or cloud schedulers (AWS EventBridge, GCP Cloud Scheduler)
+ - Typical schedule: 9:00 AM team local time, Monday-Friday
+ - Consider time zones for distributed teams
+
+2. **Error Handling**: Add robust error handling:
+ - Retry failed Slack API calls with exponential backoff
+ - Alert admins if standup fails to post or collect responses
+ - Handle missing or malformed responses gracefully
+
+3. **Non-Responders**: Send reminders:
+ - Track who responded vs. who didn't
+ - Send DM reminders 2-3 minutes before deadline
+ - Include non-responder list in summary for follow-up
+
+4. **Data Storage**: Store historical data:
+ - Save standup responses and summaries to database
+ - Track participation rates over time
+ - Enable trend analysis and reporting
+
+5. **Privacy & Security**:
+ - Store OAuth tokens securely (environment variables, secret managers)
+ - Be mindful of sensitive information in standups
+ - Consider data retention policies for historical standups
+ - Allow team members to edit/delete their responses
+
+6. **Customization per Team**:
+ - Store team configs (channel IDs, questions, timing)
+ - Allow teams to opt-in/opt-out
+ - Support different schedules for different teams
+
+7. **Performance**:
+ - Cache Slack user info to reduce API calls
+ - Implement request queuing for multiple teams
+ - Monitor token usage and costs
+
+8. **Testing**:
+ - Test in a sandbox channel first
+ - Have a manual override to skip days (holidays, etc.)
+ - Implement dry-run mode for testing prompts
+
+
+**Scheduling Tip:**
+
+Use a scheduling service to trigger the bot daily:
+
+```typescript
+// Example with node-cron
+import cron from 'node-cron';
+
+// Run at 9 AM Monday-Friday
+cron.schedule('0 9 * * 1-5', () => {
+ runStandup('C1234567890', 'C0987654321', 5);
+}, {
+ timezone: "America/New_York"
+});
+```
+
+
+## What's Next?
+
+Congratulations! You've built an AI-powered Slack standup bot that automates team updates and provides executive insights.
+
+### Learn More
+
+
+
+ Explore advanced SDK features and patterns.
+
+
+
+ Learn more about managing Slack authorization.
+
+
+
+ Monitor bot performance and standup participation.
+
+
+
+### Related Sample Projects
+
+
+
+ Build an AI code reviewer that analyzes pull requests for security issues and code quality.
+
+
+
+ Create an AI coordinator that schedules interviews and sends professional emails.
+
+
+
+
+Need help? Email us at [support@metorial.com](mailto:support@metorial.com).
+
diff --git a/sample-projects-support-bot.mdx b/sample-projects-support-bot.mdx
new file mode 100644
index 0000000..5719c5d
--- /dev/null
+++ b/sample-projects-support-bot.mdx
@@ -0,0 +1,738 @@
+---
+title: "Build a Support Bot with Slack and Zendesk"
+description: "Create an AI-powered support bot that monitors Slack, checks Zendesk tickets, and routes complex issues to humans"
+---
+
+## What You'll Build
+
+Build an intelligent support bot that:
+- Monitors your Slack support channels for customer questions
+- Automatically checks ticket status in Zendesk when customers ask
+- Uses AI to respond to common questions without human intervention
+- Routes complex issues to your support team with proper context
+
+This tutorial demonstrates real-world multi-service integration using Metorial's MCP servers.
+
+
+**What you'll learn:**
+- Deploying multiple MCP servers (Slack and Zendesk)
+- Setting up OAuth for user authentication
+- Creating an AI agent that uses tools from multiple services
+- Implementing decision logic for automated responses
+
+**Before you begin:**
+- [Create a Metorial account](/metorial-101-introduction)
+- [Create API keys](/api-getting-started)
+- Slack workspace with admin access
+- Zendesk account with API access
+- OpenAI or Anthropic API key
+
+**Time to complete:** 15-20 minutes
+
+
+## Prerequisites
+
+Before building the support bot, ensure you have:
+
+1. **Metorial setup**:
+ - Active Metorial account at [app.metorial.com](https://app.metorial.com)
+ - Project created in your organization
+ - Metorial API key (generate in Dashboard → Home → Connect to Metorial)
+
+2. **Slack workspace**:
+ - Admin access to create OAuth apps
+ - Support channel where bot will listen (e.g., `#customer-support`)
+
+3. **Zendesk account**:
+ - Admin access to create API tokens
+ - Active tickets for testing
+
+4. **AI provider**:
+ - OpenAI API key (GPT-4 recommended) OR
+ - Anthropic API key (Claude Sonnet 4.5 recommended)
+
+5. **Development environment**:
+ - Node.js 18+ (TypeScript) or Python 3.9+ installed
+ - Basic knowledge of async/await patterns
+
+## Architecture Overview
+
+The support bot follows this workflow:
+
+1. **Listen**: Monitor Slack channels for customer messages
+2. **Analyze**: AI determines if message is about ticket status, common question, or complex issue
+3. **Act**:
+ - **Ticket status**: Query Zendesk and post status to Slack
+ - **Common question**: AI generates and posts response to Slack
+ - **Complex issue**: Tag support team in dedicated channel with context
+
+**Services used:**
+- **Slack MCP Server**: Read messages, post responses, send DMs
+- **Zendesk MCP Server**: Search tickets, read ticket details, add internal notes
+- **AI Model**: Decision-making and response generation
+
+## Step 1: Deploy Slack MCP Server
+
+Deploy the Slack MCP server from Metorial's catalog to enable your bot to interact with Slack.
+
+
+
+ In the Metorial Dashboard, go to **Servers** and search for "Slack".
+
+
+
+ Click the **Slack** server, then click **Deploy Server** → **Server Deployment**.
+
+ Give your deployment a descriptive name like "Support Bot Slack".
+
+
+
+ Note your **Server Deployment ID** (shown on the deployment page). You'll need this for OAuth setup.
+
+ We'll configure Slack OAuth in Step 3.
+
+
+
+
+Keep your Slack deployment ID handy. You'll use it when setting up OAuth and writing bot code.
+
+
+## Step 2: Deploy Zendesk MCP Server
+
+Deploy the Zendesk MCP server to enable ticket querying and management.
+
+
+
+ In the Metorial Dashboard, search for "Zendesk" in the Servers section.
+
+
+
+ Click **Deploy Server** → **Server Deployment**.
+
+ Name it "Support Bot Zendesk".
+
+
+
+ Zendesk requires API token authentication:
+
+ 1. In Zendesk Admin Center, go to **Apps and Integrations** → **APIs** → **Zendesk API**
+ 2. Enable **Token Access**
+ 3. Click **Add API token**, give it a description (e.g., "Metorial Support Bot")
+ 4. Copy the token and paste it in the Metorial deployment configuration
+ 5. Enter your Zendesk subdomain (e.g., `yourcompany.zendesk.com`)
+
+
+
+ Click **Deploy**. Note your **Server Deployment ID** for use in the code.
+
+
+
+## Step 3: Set Up OAuth Authentication
+
+Your support bot needs permission to access your Slack workspace on behalf of your team.
+
+
+
+ Install the Metorial SDK and your chosen AI provider:
+
+
+ ```bash TypeScript
+ npm install metorial @metorial/anthropic @anthropic-ai/sdk
+ ```
+
+ ```bash Python
+ pip install metorial anthropic
+ ```
+
+
+
+
+ Run this code to generate the Slack OAuth URL:
+
+
+ ```typescript TypeScript
+ import { Metorial } from 'metorial';
+
+ const metorial = new Metorial({
+ apiKey: process.env.METORIAL_API_KEY
+ });
+
+ async function setupSlackOAuth() {
+ const slackOAuth = await metorial.oauth.sessions.create({
+ serverDeploymentId: 'your-slack-deployment-id',
+ callbackUrl: 'https://yourapp.com/oauth/callback' // Optional
+ });
+
+ console.log('Authorize Slack here:', slackOAuth.url);
+ console.log('OAuth Session ID:', slackOAuth.id);
+
+ // Wait for authorization
+ await metorial.oauth.waitForCompletion([slackOAuth]);
+ console.log('✓ Slack authorized!');
+
+ // Save slackOAuth.id for future use
+ return slackOAuth.id;
+ }
+
+ setupSlackOAuth();
+ ```
+
+ ```python Python
+ import asyncio
+ import os
+ from metorial import Metorial
+
+ metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY"))
+
+ async def setup_slack_oauth():
+ slack_oauth = await metorial.oauth.sessions.create(
+ server_deployment_id="your-slack-deployment-id",
+ callback_url="https://yourapp.com/oauth/callback" # Optional
+ )
+
+ print(f"Authorize Slack here: {slack_oauth.url}")
+ print(f"OAuth Session ID: {slack_oauth.id}")
+
+ # Wait for authorization
+ await metorial.oauth.wait_for_completion([slack_oauth])
+ print("✓ Slack authorized!")
+
+ # Save slack_oauth.id for future use
+ return slack_oauth.id
+
+ asyncio.run(setup_slack_oauth())
+ ```
+
+
+
+
+ 1. Open the printed OAuth URL in your browser
+ 2. Select your Slack workspace
+ 3. Review and approve the permissions
+ 4. You'll be redirected to your callback URL (or see a confirmation page)
+
+
+
+ Save the OAuth session ID securely. You'll reuse it for all future bot operations without re-authorizing.
+
+ For production apps, store OAuth session IDs in your database per user/workspace.
+
+
+
+
+**OAuth vs API Keys:**
+- **Slack** uses OAuth (user authorization)
+- **Zendesk** uses API tokens (configured in deployment)
+
+You only need OAuth setup for Slack.
+
+
+## Step 4: Build the Support Bot Core Logic
+
+Create the main bot that connects to both Slack and Zendesk with AI-powered decision making.
+
+
+```typescript TypeScript
+import { Metorial } from 'metorial';
+import { metorialAnthropic } from '@metorial/anthropic';
+import Anthropic from '@anthropic-ai/sdk';
+
+const metorial = new Metorial({
+ apiKey: process.env.METORIAL_API_KEY!
+});
+
+const anthropic = new Anthropic({
+ apiKey: process.env.ANTHROPIC_API_KEY!
+});
+
+// Store your deployment IDs and OAuth session ID
+const SLACK_DEPLOYMENT_ID = 'your-slack-deployment-id';
+const ZENDESK_DEPLOYMENT_ID = 'your-zendesk-deployment-id';
+const SLACK_OAUTH_SESSION_ID = 'your-slack-oauth-session-id';
+
+async function runSupportBot(userMessage: string, slackChannel: string) {
+ await metorial.withProviderSession(
+ metorialAnthropic,
+ {
+ serverDeployments: [
+ {
+ serverDeploymentId: SLACK_DEPLOYMENT_ID,
+ oauthSessionId: SLACK_OAUTH_SESSION_ID
+ },
+ {
+ serverDeploymentId: ZENDESK_DEPLOYMENT_ID
+ }
+ ],
+ streaming: false
+ },
+ async ({ tools, callTools, closeSession }) => {
+ // AI analyzes the message and decides what action to take
+ const messages: Anthropic.MessageParam[] = [
+ {
+ role: 'user',
+ content: `You are a support bot. A customer wrote: "${userMessage}"
+
+Analyze this message and take appropriate action:
+
+1. If asking about ticket status (e.g., "what's the status of ticket #1234"):
+ - Use Zendesk tools to search for and retrieve ticket details
+ - Post a summary to Slack channel ${slackChannel}
+
+2. If asking a common question (e.g., "how do I reset my password"):
+ - Generate a helpful response
+ - Post it to Slack channel ${slackChannel}
+
+3. If it's complex or requires human attention:
+ - Post to #support-escalations channel
+ - Tag @support-team
+ - Include customer's original message
+
+Take action now using the available tools.`
+ }
+ ];
+
+ let response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 2048,
+ messages,
+ tools
+ });
+
+ // Handle tool calls in agentic loop
+ while (response.stop_reason === 'tool_use') {
+ const toolUseBlocks = response.content.filter(
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
+ );
+
+ // Execute tools via Metorial
+ const toolResults = await callTools(toolUseBlocks);
+
+ // Add assistant response and tool results to conversation
+ messages.push(
+ { role: 'assistant', content: response.content },
+ {
+ role: 'user',
+ content: toolResults.map((result, i) => ({
+ type: 'tool_result' as const,
+ tool_use_id: toolUseBlocks[i].id,
+ content: JSON.stringify(result)
+ }))
+ }
+ );
+
+ // Continue conversation
+ response = await anthropic.messages.create({
+ model: 'claude-sonnet-4-5',
+ max_tokens: 2048,
+ messages,
+ tools
+ });
+ }
+
+ // Get final text response
+ const finalText = response.content
+ .filter((block): block is Anthropic.TextBlock => block.type === 'text')
+ .map(block => block.text)
+ .join('\n');
+
+ console.log('Bot action completed:', finalText);
+
+ await closeSession();
+ }
+ );
+}
+
+// Example usage
+runSupportBot(
+ "What's the status of ticket #1234?",
+ "#customer-support"
+);
+```
+
+```python Python
+import asyncio
+import os
+from metorial import Metorial
+from anthropic import AsyncAnthropic, MessageParam
+
+metorial = Metorial(api_key=os.getenv("METORIAL_API_KEY"))
+anthropic = AsyncAnthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
+
+# Store your deployment IDs and OAuth session ID
+SLACK_DEPLOYMENT_ID = "your-slack-deployment-id"
+ZENDESK_DEPLOYMENT_ID = "your-zendesk-deployment-id"
+SLACK_OAUTH_SESSION_ID = "your-slack-oauth-session-id"
+
+async def run_support_bot(user_message: str, slack_channel: str):
+ async with metorial.provider_session(
+ provider="anthropic",
+ server_deployments=[
+ {
+ "serverDeploymentId": SLACK_DEPLOYMENT_ID,
+ "oauthSessionId": SLACK_OAUTH_SESSION_ID
+ },
+ {"serverDeploymentId": ZENDESK_DEPLOYMENT_ID}
+ ],
+ ) as session:
+ # AI analyzes the message and decides what action to take
+ messages: list[MessageParam] = [
+ {
+ "role": "user",
+ "content": f"""You are a support bot. A customer wrote: "{user_message}"
+
+Analyze this message and take appropriate action:
+
+1. If asking about ticket status (e.g., "what's the status of ticket #1234"):
+ - Use Zendesk tools to search for and retrieve ticket details
+ - Post a summary to Slack channel {slack_channel}
+
+2. If asking a common question (e.g., "how do I reset my password"):
+ - Generate a helpful response
+ - Post it to Slack channel {slack_channel}
+
+3. If it's complex or requires human attention:
+ - Post to #support-escalations channel
+ - Tag @support-team
+ - Include customer's original message
+
+Take action now using the available tools."""
+ }
+ ]
+
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=2048,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Handle tool calls in agentic loop
+ while response.stop_reason == "tool_use":
+ tool_use_blocks = [
+ block for block in response.content if block.type == "tool_use"
+ ]
+
+ # Execute tools via Metorial
+ tool_results = await session.call_tools(tool_use_blocks)
+
+ # Add assistant response and tool results to conversation
+ messages.append({"role": "assistant", "content": response.content})
+ messages.append({
+ "role": "user",
+ "content": [
+ {
+ "type": "tool_result",
+ "tool_use_id": tool_use_blocks[i].id,
+ "content": str(result)
+ }
+ for i, result in enumerate(tool_results)
+ ]
+ })
+
+ # Continue conversation
+ response = await anthropic.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=2048,
+ tools=session.tools,
+ messages=messages,
+ )
+
+ # Get final text response
+ final_text = "\n".join(
+ block.text for block in response.content if block.type == "text"
+ )
+
+ print(f"Bot action completed: {final_text}")
+
+# Example usage
+asyncio.run(run_support_bot(
+ "What's the status of ticket #1234?",
+ "#customer-support"
+))
+```
+
+
+**What this code does:**
+
+1. **Creates a provider session** with both Slack and Zendesk tools available
+2. **Sends customer message to AI** with instructions on how to handle different scenarios
+3. **AI decides and executes tools**:
+ - Searches Zendesk if ticket status requested
+ - Posts to Slack with responses
+ - Routes to support team if needed
+4. **Handles multi-step tool calls** in an agentic loop until complete
+
+
+This example uses **Claude's agentic capabilities**—the AI autonomously decides which tools to call and when. You don't need to write routing logic yourself.
+
+
+## Step 5: Check Ticket Status
+
+Let's test the bot with a ticket status inquiry.
+
+**Scenario**: Customer asks "What's the status of my ticket #1234?"
+
+The bot will:
+1. Parse ticket number from message
+2. Call Zendesk's `search_tickets` or `get_ticket` tool
+3. Format ticket details (status, assignee, last update)
+4. Post to Slack
+
+
+```typescript TypeScript
+// Test ticket status check
+await runSupportBot(
+ "Hi! Can you check the status of ticket #1234?",
+ "#customer-support"
+);
+
+// Expected flow:
+// 1. AI calls Zendesk tool: zendesk_get_ticket(ticket_id: 1234)
+// 2. AI formats response: "Ticket #1234 is currently 'In Progress' and assigned to Sarah. Last updated 2 hours ago."
+// 3. AI calls Slack tool: slack_post_message(channel: "#customer-support", text: "...")
+```
+
+```python Python
+# Test ticket status check
+await run_support_bot(
+ "Hi! Can you check the status of ticket #1234?",
+ "#customer-support"
+)
+
+# Expected flow:
+# 1. AI calls Zendesk tool: zendesk_get_ticket(ticket_id: 1234)
+# 2. AI formats response: "Ticket #1234 is currently 'In Progress' and assigned to Sarah. Last updated 2 hours ago."
+# 3. AI calls Slack tool: slack_post_message(channel: "#customer-support", text: "...")
+```
+
+
+
+The AI autonomously chains tool calls:
+1. First calls `zendesk_get_ticket`
+2. Receives ticket data
+3. Then calls `slack_post_message` with formatted response
+
+This is the power of agentic AI with multi-tool access.
+
+
+## Step 6: AI-Powered Auto-Response
+
+Test the bot with a common question that doesn't require tools.
+
+**Scenario**: Customer asks "How do I reset my password?"
+
+The bot will:
+1. Recognize this as a common question
+2. Generate helpful response using its knowledge
+3. Post directly to Slack
+
+
+```typescript TypeScript
+// Test auto-response for common question
+await runSupportBot(
+ "How do I reset my password?",
+ "#customer-support"
+);
+
+// Expected flow:
+// 1. AI recognizes common question (no Zendesk needed)
+// 2. AI generates response with password reset steps
+// 3. AI calls Slack tool: slack_post_message(channel: "#customer-support", text: "To reset your password: 1. Go to...")
+```
+
+```python Python
+# Test auto-response for common question
+await run_support_bot(
+ "How do I reset my password?",
+ "#customer-support"
+)
+
+# Expected flow:
+# 1. AI recognizes common question (no Zendesk needed)
+# 2. AI generates response with password reset steps
+# 3. AI calls Slack tool: slack_post_message(channel: "#customer-support", text: "To reset your password: 1. Go to...")
+```
+
+
+
+**Customizing Auto-Responses:**
+
+For production bots, enhance the system prompt with:
+- Your company's knowledge base articles
+- FAQs and standard responses
+- Links to help documentation
+- Escalation criteria (e.g., "always escalate billing questions")
+
+
+## Step 7: Route Complex Issues to Humans
+
+Test the bot with a complex issue that needs human attention.
+
+**Scenario**: Customer reports "Your API is returning 500 errors and our production is down!"
+
+The bot will:
+1. Recognize this as urgent and complex
+2. Post to escalation channel
+3. Tag support team
+4. Include original message and urgency
+
+
+```typescript TypeScript
+// Test escalation for complex issue
+await runSupportBot(
+ "Your API is returning 500 errors and our production is down!",
+ "#customer-support"
+);
+
+// Expected flow:
+// 1. AI recognizes urgency and complexity
+// 2. AI calls Slack tool: slack_post_message(
+// channel: "#support-escalations",
+// text: "🚨 URGENT: Production issue reported by customer..."
+// mentions: ["@support-team"]
+// )
+// 3. AI calls Slack tool: slack_post_message(
+// channel: "#customer-support",
+// text: "I've escalated this to our engineering team. Someone will assist you shortly."
+// )
+```
+
+```python Python
+# Test escalation for complex issue
+await run_support_bot(
+ "Your API is returning 500 errors and our production is down!",
+ "#customer-support"
+)
+
+# Expected flow:
+# 1. AI recognizes urgency and complexity
+# 2. AI calls Slack tool: slack_post_message(
+# channel="#support-escalations",
+# text="🚨 URGENT: Production issue reported by customer..."
+# mentions=["@support-team"]
+# )
+# 3. AI calls Slack tool: slack_post_message(
+# channel="#customer-support",
+# text="I've escalated this to our engineering team. Someone will assist you shortly."
+# )
+```
+
+
+
+**Configuring Escalation Rules:**
+
+Update the system prompt to define when to escalate:
+- Keywords: "down", "urgent", "billing", "refund", "legal"
+- Customer sentiment: angry or frustrated tone
+- Business rules: questions about enterprise plans, integrations
+
+
+## Step 8: Test End-to-End
+
+Run a complete test to verify all functionality.
+
+
+
+ Open your Slack workspace and watch the `#customer-support` channel.
+
+
+
+ Run the bot with different test messages:
+
+
+ ```typescript TypeScript
+ // Test all scenarios
+ await runSupportBot("What's the status of ticket #1234?", "#customer-support");
+ await runSupportBot("How do I reset my password?", "#customer-support");
+ await runSupportBot("API is down in production!", "#customer-support");
+ ```
+
+ ```python Python
+ # Test all scenarios
+ await run_support_bot("What's the status of ticket #1234?", "#customer-support")
+ await run_support_bot("How do I reset my password?", "#customer-support")
+ await run_support_bot("API is down in production!", "#customer-support")
+ ```
+
+
+
+
+ Check that:
+ - ✓ Ticket status posted to Slack with correct details
+ - ✓ Password reset instructions posted clearly
+ - ✓ Urgent issue escalated to #support-escalations
+ - ✓ Acknowledgment posted to customer in original channel
+
+
+
+ Verify the bot can read tickets:
+ 1. Go to your Zendesk dashboard
+ 2. Create a test ticket
+ 3. Ask the bot about that ticket ID
+ 4. Confirm details match
+
+
+
+
+**Debugging Tips:**
+
+If tools aren't being called:
+- Check OAuth session is active (try re-authorizing)
+- Verify deployment IDs are correct
+- Check API tokens for Zendesk haven't expired
+- Review Metorial dashboard logs under Monitoring
+
+
+## What's Next?
+
+Congratulations! You've built a production-ready support bot that integrates Slack and Zendesk with AI-powered decision making.
+
+### Enhancements to Try
+
+
+
+ Use Slack's Events API to trigger bot on new messages automatically instead of manual calls.
+
+
+
+ Enhance AI prompt to detect frustrated customers and escalate proactively.
+
+
+
+ Allow bot to create new tickets when customers report issues without ticket IDs.
+
+
+
+ Add language detection and translation for global support teams.
+
+
+
+### Learn More
+
+
+
+ Explore advanced SDK features and patterns.
+
+
+
+ Learn more about managing user authorizations.
+
+
+
+ Monitor tool calls and debug issues.
+
+
+
+### Production Considerations
+
+Before deploying to production:
+
+1. **Error handling**: Add try/catch blocks and retry logic
+2. **Rate limiting**: Implement backoff for high-volume channels
+3. **Security**: Store API keys in environment variables or secrets manager
+4. **Monitoring**: Set up alerts for failed tool calls
+5. **Testing**: Create automated tests for common scenarios
+
+
+Need help? Email us at [support@metorial.com](mailto:support@metorial.com).
+
diff --git a/sdk-oauth.mdx b/sdk-oauth.mdx
index 82a23a4..493eca9 100644
--- a/sdk-oauth.mdx
+++ b/sdk-oauth.mdx
@@ -203,7 +203,7 @@ let slackOAuth = await metorial.oauth.sessions.create({
console.log('Please authorize:', slackOAuth.url);
// 3. Wait for user to complete OAuth
-await metorial.oauth.waitForCompletion([slackOAuth.id]);
+await metorial.oauth.waitForCompletion([slackOAuth]);
// 4. Store the OAuth session ID in your database
// database.saveOAuthSession(userId, slackOAuth.id);
@@ -244,7 +244,7 @@ async def main():
print(f"Please authorize: {slack_oauth.url}")
# 3. Wait for user to complete OAuth
- await metorial.oauth.wait_for_completion([slack_oauth.id])
+ await metorial.oauth.wait_for_completion([slack_oauth])
# 4. Store the OAuth session ID in your database
# database.save_oauth_session(user_id, slack_oauth.id)
diff --git a/sdk-quickstart.mdx b/sdk-quickstart.mdx
index 99e1422..c35f324 100644
--- a/sdk-quickstart.mdx
+++ b/sdk-quickstart.mdx
@@ -182,7 +182,7 @@ console.log('Authorize Slack:', slackOAuth.url);
console.log('Authorize Calendar:', calendarOAuth.url);
// 3. Wait for user to complete OAuth
-await metorial.oauth.waitForCompletion([slackOAuth.id, calendarOAuth.id]);
+await metorial.oauth.waitForCompletion([slackOAuth, calendarOAuth]);
// 4. Use authenticated sessions
await metorial.withProviderSession(
@@ -227,7 +227,7 @@ async def main():
print(f"Authorize Slack: {slack_oauth.url}")
# 3. Wait for completion
- await metorial.oauth.wait_for_completion([slack_oauth.id])
+ await metorial.oauth.wait_for_completion([slack_oauth])
# 4. Use authenticated session
async def handler(session):