The Step-by-Step Guide to Automating Custom Bob Modes with YAML and Instructions
Introduction
In the world of AI-assisted development, a one-size-fits-all approach rarely works. A prompt that makes an excellent creative writer might make a terrible backend developer. That is where Bob Modes come in.
At its core, a Bob Mode is a specialized persona configuration that tailors the AI’s behavior, instructions, and tool access to a highly specific task or environment. Think of it as changing Bob’s “job description” on the fly depending on what you are building.
What are the Different Bob Modes?
Bob can shift between several standard and custom personas, each equipped with its own unique systemic instructions (via roleDefinition) and tool permissions (via groups like mcp, read, or terminal):
- Standard Development Modes: Focused on general coding, refactoring, and debugging across standard languages.
- Architect / Reviewer Modes: Geared toward high-level system design, code reviews, and enforcement of styling guidelines rather than raw code generation.
- System Admin / DevOps Modes: Granted broader access to terminal tools and system commands to help manage infrastructure, deployments, or local environments.
- Custom & Ad-Hoc Modes: User-created personas (like the “Simple API” or “Python Data Scientist” modes) designed to tackle niche frameworks, internal APIs, or temporary project tasks.
Why Use Specific Modes?
Switching to a specific, targeted mode instead of using a generic chat assistant offers several massive advantages for your workflow:
-
Context Optimization: AI models have finite context windows. By isolating instructions into specific modes (and attaching targeted rule markdown files in
.bob/rules-{slug}), you ensure Bob only focuses on relevant constraints—saving tokens and memory. -
Guardrails and Safety: You can restrict a mode’s tool access. For example, a “Documentation Mode” might only need
readaccess to files, entirely removing its ability to accidentally run destructive commands in yourterminal. -
Team Standardization: By committing a custom mode (like
.bob/custom_modes.yaml) directly to your Git repository, your entire engineering team instantly gains access to an AI assistant that seamlessly understands your project's exact architecture, naming conventions, and tech stack. - Reduced Prompt Engineering: Instead of pasting a massive “You are a senior React developer who must use TypeScript and Tailwind…” primer at the start of every single chat session, a specific mode ensures Bob wakes up already knowing the rules.
Creating a Bob Mode — Implementation
The sample application consists of an isolated microservice ecosystem designed to showcase localized, tool-driven development. Rather than hardcoding capabilities inside a chat window, the aim is to expose to the local operating system profiles and service hooks directly to our agentic layer using a clean four-part structural setup:
+-------------------------------------------------------+
| IBM BOB UI |
| (Switches to custom-mode & triggers MCP execution) |
+---------------------------+---------------------------+
|
v
+-------------------------------------------------------+
| server.py (FastMCP Bridge) |
| - Exposes Python functions as formal tools |
| - Acts as the standard semantic gateway |
+---------------------------+---------------------------+
|
v
+-------------------------------------------------------+
| app.py (Core Flask Engine) |
| - Runs locally at http://localhost:8080 |
| - Pulls OS platform profiles and local system time |
+-------------------------------------------------------+
Which once implemented becomes the following architecture;
🏗️ Step 1: Building the Infrastructure
To implement the function and boundaries, the aim is to establish a clean and modular local environment.
-
app.py(The Core Engine): A local Flask server hosting system utilities and orchestrating core operations. -
server.py(The MCP Bridge): Built with FastMCP, this service auto-discovers Python functions and registers them as valid tools exposed directly to the AI runtime. -
agent.py(The Orchestrator): Handles client-side connections, discovering capabilities programmatically. -
.bob/custom_modes.yaml(The Self-Generated Persona): The final workspace configuration file generated by Bob, for Bob, defining his operational scope.
Bob-create-Bobmode/
├── Docs/ # Documentation
│ └── Architecture.md # Detailed architecture
├── input/ # Input files (if needed)
├── output/ # Output files (timestamped)
├── scripts/ # Automation scripts
│ ├── setup.sh # Setup environment
│ ├── start_api.sh # Start REST API
│ └── test_agent.sh # Test agent
├── src/
│ ├── api/ # REST API application
│ │ ├── app.py # Flask application
│ │ └── requirements.txt # API dependencies
│ ├── mcp_server/ # MCP server
│ │ ├── server.py # MCP server implementation
│ │ └── requirements.txt # MCP dependencies
│ ├── agent/ # Test agent
│ │ ├── agent.py # Agent implementation
│ │ └── requirements.txt # Agent dependencies
│ └── bob_mode/ # Bob mode configuration
│ ├── mode.yaml # Mode configuration (YAML format)
│ ├── mode.json # Legacy JSON format (deprecated)
│ └── BOB_MODE_CREATION_GUIDE.md # Detailed guide
├── .gitignore # Git ignore rules
└── README.md #
1-The Local Service Engine (app.py)
This file sets up our local endpoints, acting as a gateway for fetching system information, monitoring API statuses, or retrieving structural metrics.
Basically an application accessible through REST API calls.
# app.py
"""
Simple REST API Application
Provides 3 basic endpoints for demonstration purposes
"""
from flask import Flask, jsonify, request
from datetime import datetime
import os
import platform
app = Flask(__name__)
@app.route('/api/time', methods=['GET'])
def get_local_time():
"""
Get current local time
Returns: JSON with current timestamp and formatted time
"""
now = datetime.now()
return jsonify({
'timestamp': now.isoformat(),
'formatted': now.strftime('%Y-%m-%d %H:%M:%S'),
'timezone': datetime.now().astimezone().tzname()
})
@app.route('/api/files', methods=['GET'])
def list_files():
"""
List files in a specified directory
Query params: path (optional, defaults to current directory)
Returns: JSON with list of files and directories
"""
directory = request.args.get('path', '.')
try:
# Security: prevent directory traversal
directory = os.path.abspath(directory)
items = []
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
items.append({
'name': item,
'type': 'directory' if os.path.isdir(item_path) else 'file',
'size': os.path.getsize(item_path) if os.path.isfile(item_path) else None
})
return jsonify({
'directory': directory,
'count': len(items),
'items': items
})
except Exception as e:
return jsonify({'error': str(e)}), 400
@app.route('/api/system', methods=['GET'])
def get_system_info():
"""
Get basic system information
Returns: JSON with system details
"""
return jsonify({
'platform': platform.system(),
'platform_release': platform.release(),
'platform_version': platform.version(),
'architecture': platform.machine(),
'processor': platform.processor(),
'python_version': platform.python_version()
})
@app.route('/api/health', methods=['GET'])
def health_check():
"""
Health check endpoint
Returns: JSON with status
"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat()
})
@app.route('/', methods=['GET'])
def index():
"""
Root endpoint with API documentation
"""
return jsonify({
'name': 'Simple REST API',
'version': '1.0.0',
'endpoints': {
'/api/time': 'Get current local time',
'/api/files': 'List files in directory (query param: path)',
'/api/system': 'Get system information',
'/api/health': 'Health check'
}
})
if __name__ == '__main__':
# Use port 8080 instead of 5000 (reserved on macOS)
app.run(host='0.0.0.0', port=8080, debug=True)
# Made with Bob
Simple tests are provided to ensure that the application runs correctly.
curl "http://localhost:8080/api/files?path=."
{
"directory": "/Users/username/project",
"count": 5,
"items": [
{
"name": "README.md",
"type": "file",
"size": 1234
},
{
"name": "src",
"type": "directory",
"size": null
}
]
}
curl http://localhost:8080/api/time
{
"timestamp": "2026-06-08T12:30:45.123456",
"formatted": "2026-06-08 12:30:45",
"timezone": "CEST"
}
2-The Model Context Protocol Gateway (server.py)
Using FastMCP, we turn arbitrary functional components into formal specifications that can be ingested, understood, and executed by an AI model.
# server.py
"""
MCP Server for REST API Access
Provides tools to interact with the Simple REST API
"""
import asyncio
import json
import requests
from typing import Any
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
from mcp.server.stdio import stdio_server
from mcp.types import (
Tool,
TextContent,
ImageContent,
EmbeddedResource,
)
# API Base URL
API_BASE_URL = "http://localhost:8080"
# Create server instance
server = Server("simple-api-mcp-server")
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
"""
List available tools for the MCP server
"""
return [
Tool(
name="get_local_time",
description="Get the current local time from the API",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="list_directory_files",
description="List files and directories in a specified path",
inputSchema={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list (defaults to current directory)"
}
},
"required": []
}
),
Tool(
name="get_system_info",
description="Get system information from the API",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="health_check",
description="Check if the API is healthy and responding",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
)
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent | ImageContent | EmbeddedResource]:
"""
Handle tool execution requests
"""
try:
if name == "get_local_time":
response = requests.get(f"{API_BASE_URL}/api/time", timeout=5)
response.raise_for_status()
data = response.json()
return [
TextContent(
type="text",
text=json.dumps(data, indent=2)
)
]
elif name == "list_directory_files":
path = arguments.get("path", ".")
response = requests.get(
f"{API_BASE_URL}/api/files",
params={"path": path},
timeout=5
)
response.raise_for_status()
data = response.json()
return [
TextContent(
type="text",
text=json.dumps(data, indent=2)
)
]
elif name == "get_system_info":
response = requests.get(f"{API_BASE_URL}/api/system", timeout=5)
response.raise_for_status()
data = response.json()
return [
TextContent(
type="text",
text=json.dumps(data, indent=2)
)
]
elif name == "health_check":
response = requests.get(f"{API_BASE_URL}/api/health", timeout=5)
response.raise_for_status()
data = response.json()
return [
TextContent(
type="text",
text=json.dumps(data, indent=2)
)
]
else:
raise ValueError(f"Unknown tool: {name}")
except requests.exceptions.RequestException as e:
return [
TextContent(
type="text",
text=f"Error calling API: {str(e)}"
)
]
except Exception as e:
return [
TextContent(
type="text",
text=f"Error: {str(e)}"
)
]
async def main():
"""
Main entry point for the MCP server
"""
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="simple-api-mcp-server",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
)
)
)
if __name__ == "__main__":
asyncio.run(main())
# Made with Bob
3-An Agent to Interact with our System through the MCP Server (agent.py)
# agent.py
"""
Simple Agent using MCP Server
Demonstrates how to interact with the MCP server to access the REST API
"""
import asyncio
import json
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
class SimpleAgent:
"""
A simple agent that uses MCP to interact with the REST API
"""
def __init__(self, server_script_path: str):
"""
Initialize the agent with the MCP server path
Args:
server_script_path: Path to the MCP server script
"""
self.server_script_path = server_script_path
self.session = None
async def connect(self):
"""
Connect to the MCP server
"""
server_params = StdioServerParameters(
command="python",
args=[self.server_script_path],
env=None
)
# Store the context manager for later use
self.stdio_context = stdio_client(server_params)
self.stdio, self.write = await self.stdio_context.__aenter__()
self.session = ClientSession(self.stdio, self.write)
await self.session.initialize()
print("✓ Connected to MCP server")
async def list_available_tools(self):
"""
List all available tools from the MCP server
"""
if not self.session:
raise RuntimeError("Not connected to MCP server")
tools = await self.session.list_tools()
print("\n📋 Available Tools:")
for tool in tools:
print(f" - {tool.name}: {tool.description}")
return tools
async def call_tool(self, tool_name: str, arguments: dict = None):
"""
Call a specific tool on the MCP server
Args:
tool_name: Name of the tool to call
arguments: Arguments to pass to the tool
Returns:
Tool execution result
"""
if not self.session:
raise RuntimeError("Not connected to MCP server")
if arguments is None:
arguments = {}
print(f"\n🔧 Calling tool: {tool_name}")
if arguments:
print(f" Arguments: {json.dumps(arguments, indent=2)}")
result = await self.session.call_tool(tool_name, arguments)
print(f"✓ Result:")
for content in result.content:
if hasattr(content, 'text'):
print(content.text)
return result
async def disconnect(self):
"""
Disconnect from the MCP server
"""
if self.session:
await self.session.close()
if hasattr(self, 'stdio_context'):
await self.stdio_context.__aexit__(None, None, None)
print("\n✓ Disconnected from MCP server")
async def run_demo(self):
"""
Run a demonstration of the agent's capabilities
"""
try:
# Connect to MCP server
await self.connect()
# List available tools
await self.list_available_tools()
# Demo 1: Health check
print("\n" + "="*60)
print("DEMO 1: Health Check")
print("="*60)
await self.call_tool("health_check")
# Demo 2: Get local time
print("\n" + "="*60)
print("DEMO 2: Get Local Time")
print("="*60)
await self.call_tool("get_local_time")
# Demo 3: Get system info
print("\n" + "="*60)
print("DEMO 3: Get System Information")
print("="*60)
await self.call_tool("get_system_info")
# Demo 4: List files in current directory
print("\n" + "="*60)
print("DEMO 4: List Files in Current Directory")
print("="*60)
await self.call_tool("list_directory_files", {"path": "."})
except Exception as e:
print(f"\n❌ Error: {str(e)}")
finally:
await self.disconnect()
async def main():
"""
Main entry point for the agent
"""
import sys
import os
# Get the MCP server script path
script_dir = os.path.dirname(os.path.abspath(__file__))
server_path = os.path.join(script_dir, "..", "mcp_server", "server.py")
print("="*60)
print("Simple Agent - MCP Demo")
print("="*60)
print(f"MCP Server: {server_path}")
print("="*60)
# Create and run the agent
agent = SimpleAgent(server_path)
await agent.run_demo()
if __name__ == "__main__":
asyncio.run(main())
# Made with Bob
- Connecting to the MCP server, we receive something like:
[
{
"name": "get_local_time",
"description": "Get the current local time from the API",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "list_directory_files",
"description": "List files and directories in a specified path",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list"
}
},
"required": []
}
}
]
- And as an example of tool calling for MCP request we send;
{
"method": "tools/call",
"params": {
"name": "get_local_time",
"arguments": {}
}
}
- The MCP response is;
{
"content": [
{
"type": "text",
"text": "{\n \"timestamp\": \"2026-06-08T12:30:45.123456\",\n \"formatted\": \"2026-06-08 12:30:45\",\n \"timezone\": \"CEST\"\n}"
}
]
}
- And when running our agent to use the MCP server in order to get the list of tools, we'll see;
python src/agent/agent.py
============================================================
Simple Agent - MCP Demo
============================================================
MCP Server: ../mcp_server/server.py
============================================================
✓ Connected to MCP server
📋 Available Tools:
- get_local_time: Get the current local time from the API
- list_directory_files: List files and directories in a specified path
- get_system_info: Get system information from the API
- health_check: Check if the API is healthy and responding
============================================================
DEMO 1: Health Check
============================================================
🔧 Calling tool: health_check
✓ Result:
{
"status": "healthy",
"timestamp": "2026-06-08T12:30:45.123456"
}
============================================================
DEMO 2: Get Local Time
============================================================
🔧 Calling tool: get_local_time
✓ Result:
{
"timestamp": "2026-06-08T12:30:45.123456",
"formatted": "2026-06-08 12:30:45",
"timezone": "CEST"
}
✓ Disconnected from MCP server
🤖 Step 2: Activating the Inception Prompt
With the core services active and logging to our terminal, we don’t write configuration manifests by hand. Instead, we open Bob directly inside our editor workspace and leverage context-driven prompt engineering to make him self-configure.
Here is the exact metaprompt used to trigger the build:
- The Inception Prompt: “Bob, analyze our current workspace directory structures, specifically
app.py,server.py, and our architectural goals inArchitecture.md. Based on the microservices available and ourFastMCPtool configurations, generate a project-scoped custom mode for yourself. Save it under .bob/custom_modes.yaml. Assign it the slug simple-api, limit tool access groups exclusively to [mcp,read,terminal], and define a system role context that restricts your persona to managing these system boundaries efficiently."
📄 Step 3: Inspecting the Artifact (.bob/custom_modes.yaml)
Bob processes the request, inspects the tools registered on the server, and creates the configuration. It is important to note that recent updates to IBM Bob have shifted project configuration paradigms from legacy JSON maps to highly readable YAML definitions.
Bob automatically compiles and drops the following clean file into your project root:
# .bob/custom_modes.yaml
# IBM Bob Custom Modes Configuration
# Project-specific custom mode for Simple API
customModes:
- slug: simple-api
name: 🔌 Simple API
roleDefinition: |
You are a specialized assistant that can interact with a Simple REST API through an MCP server.
You have access to tools that allow you to:
1. Get current local time
2. List files in directories
3. Get system information
4. Check API health status
Use these tools to help users interact with the API and retrieve information.
The MCP server connects to a REST API running on http://localhost:8080.
groups:
- mcp
- read
- terminal
# Made natively with Bob
⚡ Step 4: Verification and Live Execution
Once the file lands in the .bob/ directory, the IDE context registers the system state change instantly.
- Immediate UI Availability: The custom mode dropdown panel in the bottom-left corner updates automatically to reveal your newly tailored persona: 🔌 Simple API.
-
Context Window Optimization: Selecting this mode loads only the specific parameters defined in the
roleDefinition. This prevents your context window from becoming cluttered with unnecessary boilerplate guidelines, keeping conversations lean and fast. -
Execution Loop Verification: For example, when you ask Bob, “What is the state of our local endpoints right now?”, he bypasses standard generative assumptions. Because he is explicitly locked into this mode, he natively runs the underlying
check_api_status()tool via MCP, collects the telemetry from Flask, and maps the output in real-time.
# Terminal logs showing the active server loops sync seamlessly
* Serving Flask app 'app' (Lazy loading)
* Debug mode: on
* Running on http://127.0.0.1:8080
[808] Custom modes reloaded. Ad-hoc mode 'Simple API' is now active.
Conclusion
Ultimately, configuring a dedicated workspace environment transforms your AI assistant from a generic sounding board into a deeply embedded team member. By anchoring this setup to local architecture — bridging a native Flask API through the Model Context Protocol directly into custom IDE controls — you eliminate tedious prompt engineering and ensure your development environment adapts contextually to your specific stack. Transitioning to a declarative .bob/custom_modes.yaml strategy means you no longer hand-craft strict boundaries or permission layers; you simply let your tools analyze your project and construct their own personas. Implementing these targeted modes enforces absolute security isolation, keeps your context windows incredibly lean, and ensures that any teammate pulling from Git instantly inherits an optimized, self-synchronizing pairing partner designed exactly for the mission at hand.
>>> Thanks for reading <<<
Links
- Repository for this post: https://github.com/aairom/Bob-create-Bobmode
- IBM Bob: https://bob.ibm.com/






Top comments (0)