DEV Community

Alain Airom (Ayrom)
Alain Airom (Ayrom)

Posted on

Inception Mode: How I Got IBM Bob to Build His Own Custom ‘Bob Mode’

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 read access to files, entirely removing its ability to accidentally run destructive commands in your terminal.
  • 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   |
+-------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

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                      # 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
curl http://localhost:8080/api/time

{
  "timestamp": "2026-06-08T12:30:45.123456",
  "formatted": "2026-06-08 12:30:45",
  "timezone": "CEST"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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": []
    }
  }
]
Enter fullscreen mode Exit fullscreen mode
  • And as an example of tool calling for MCP request we send;
{
  "method": "tools/call",
  "params": {
    "name": "get_local_time",
    "arguments": {}
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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}"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

🤖 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 in Architecture.md. Based on the microservices available and our FastMCP tool 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
Enter fullscreen mode Exit fullscreen mode

⚡ 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.
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)