Skip to main content

Create an AI Agent

Build an AI Agent that runs on-device with Model Context Protocol (MCP) tools. This tutorial uses create-mim to scaffold a complete AI Agent project.

New to AI Agent mims?

If you haven't already, read How AI Agent mims Work first to understand the architecture and core concepts. This tutorial puts those concepts into practice.

What You'll Build

An AI Agent mim that:

  • Exposes an MCP server with custom tools
  • Uses @mimik/agent-kit for AI orchestration
  • Streams responses via Server-Sent Events (SSE)
  • Discovers nearby devices using mimOE's mesh capabilities

Prerequisites

Before starting, ensure you have:

  • Node.js 18+ installed
  • mimOE running with the AI Foundation Addon (see Quick Start)
  • A model with tool use support loaded for inference (see below)

Verify mimOE is running:

curl http://localhost:8083/mimik-ai/openai/v1/models \
-H "Authorization: Bearer 1234"

Model Requirements for Function Calling

Model Must Support Tool Use

Not all models support function calling (tool use). The model must be specifically trained for tool use capabilities. Models without this training cannot be used for AI Agents that need to call MCP tools.

Recommended model: Qwen3 (e.g., qwen3-1.7b). It provides both reasoning and tool use support, optimized for on-device inference.

Context Size Requirements

Tool use requires a larger context window because the conversation includes tool definitions, tool calls, and tool results. Two settings control this:

  1. initContextSize (Model Registry): Total context window size (input + output combined). Set when uploading the model:

    {
    "initContextSize": 12000
    }

    For tool-using agents, 12K+ tokens is recommended. See Model Registry API.

  2. max_tokens (Inference/agent-kit): Maximum tokens to generate in the response. Set in your agent configuration:

    llm: {
    max_tokens: 4096, // Maximum output tokens
    // ... other config
    }

    This limits output length, not total context. See Inference API.

tip

initContextSize is the total budget. Your input (instructions + tool definitions + history) uses part of it, leaving the rest for output. If your input uses 8K tokens and initContextSize is 12K, you have 4K tokens available for the response.

Step 1: Create the Project

Use create-mim to scaffold an AI Agent project:

npx create-mim create my-agent -t ai-agent

This creates a my-agent/ directory with the complete project structure.

Navigate to the project and install dependencies:

cd my-agent
npm install

Step 2: Project Structure

The generated project contains:

my-agent/
├── src/
│ ├── index.js # Router with /mcp, /healthcheck, /chat endpoints
│ ├── agent.js # AI Agent orchestration
│ └── tools.js # MCP tools definition
├── config/
│ └── default.yml # Package configuration
├── test/
│ └── simple.http # REST client tests
├── package.json
├── webpack.config.js
└── eslint.config.js

Step 3: Understanding the Code

Entry Point (src/index.js)

The entry point sets up three endpoints with SSE streaming support:

const Router = require('router');
const { tools, getMcpEndpoints } = require('./tools');
const agent = require('./agent');

let isHeaderSet = false;

const sendSSE = (res, data) => {
if (!isHeaderSet) {
res.statusCode = 200;
res.setHeader('content-type', 'text/event-stream');
res.setHeader('transfer-encoding', 'chunked');
isHeaderSet = true;
}

const message = `data: ${JSON.stringify(data)}\n\n`;
res.write(message);
};

const app = Router();

// MCP protocol handler
app.post('/mcp', (req, res) => {
tools.handleMcpRequest(req.body).then((response) => {
if (response) {
res.end(JSON.stringify(response));
} else {
res.statusCode = 204;
res.end();
}
}).catch((error) => {
res.statusCode = 500;
res.end(JSON.stringify({ error }));
});
});

// Health check
app.get('/healthcheck', (req, res) => {
const { info } = global.context;
res.end(JSON.stringify({ status: 'ok', info }));
});

// Chat endpoint with SSE streaming
app.post('/chat', async (req, res) => {
try {
const result = await agent.run(getMcpEndpoints());

for await (const event of result) {
sendSSE(res, event);
}

res.end();
} catch (error) {
console.error('Agent error:', error);
if (isHeaderSet) {
sendSSE(res, { error: { message: error.message, type: 'agent_error' } });
res.end();
} else {
res.end(JSON.stringify({ error: { message: error.message, type: 'agent_error' } }));
}
}
});

mimikModule.exports = (context, req, res) => {
global.context = context;
global.http = global.context.http;
app(req, res, (e) => {
res.end(JSON.stringify({ code: e ? 400 : 404, message: e || 'Not Found' }));
});
};

Key points:

  • mimikModule.exports is the mim entry point (not module.exports)
  • The context object provides runtime APIs like context.http and context.info
  • sendSSE helper handles SSE formatting with proper headers
  • Error handling for both streaming and non-streaming error responses

MCP Tools (src/tools.js)

Why MCP for Local Tools?

agent-kit only accepts tool definitions via the MCP protocol. Even for tools defined in the same mim, we use mcp-kit to create an MCP server endpoint. This design provides:

  • Observability: All tool calls go through the API gateway and are logged automatically
  • Reusability: The same MCP server can serve multiple agents
  • Testability: Tools can be tested independently via HTTP
  • Standard protocol: MCP is portable across different agent frameworks

Define tools the AI Agent can use with @mimik/mcp-kit:

const { McpServer, z } = require('@mimik/mcp-kit');

const server = new McpServer({
name: 'minsight MCP server',
version: '1.0.0',
});

// HTTP request helper using context.http
function requestAction(opt) {
return new Promise((resolve, reject) => {
global.http.request({
url: opt.url,
type: opt.method,
headers: opt.headers,
authorization: opt.authorization,
data: opt.jsonBody && JSON.stringify(opt.jsonBody),
success: (result) => resolve(result),
error: (err) => {
if (err instanceof Error) {
reject(err);
} else {
reject(new Error(err.content || err.message));
}
},
});
});
}

// Simple tool: add two numbers
server.tool('add', 'Add two numbers', { a: z.number(), b: z.number() }, (args) => Promise.resolve({
content: [{ type: 'text', text: String(args.a + args.b) }],
}));

// Discovery tool: find nearby nodes
server.tool('discoverLocal', 'Discover nodes around me', {}, async () => {
const { httpPort } = global.context.info;
const { INSIGHT_API_KEY } = global.context.env;
try {
const res = await requestAction({
url: `http://127.0.0.1:${httpPort}/mimik-mesh/insight/v1/nodes?type=linkLocal`,
headers: { 'Authorization': `Bearer ${INSIGHT_API_KEY}` },
});

return {
content: [{ type: 'text', text: res.data }],
};
} catch (err) {
return {
content: [{ type: 'text', text: err.message }],
};
}
});

// Returns MCP endpoints for the agent to connect to
function getMcpEndpoints() {
const { info } = global.context;
const { httpPort, apiRoot } = info;

const mcpEndpoints = [{
name: 'minsight mcp',
url: `http://127.0.0.1:${httpPort}${apiRoot}/mcp`,
}];

return { mcpEndpoints };
}

module.exports = {
tools: server,
getMcpEndpoints,
};

Key components:

  • McpServer: Creates an MCP server that handles JSON-RPC protocol
  • requestAction: Helper to make HTTP requests using context.http
  • server.tool(): Registers tools with name, description, schema, and handler
  • getMcpEndpoints(): Returns endpoints the agent will connect to

Schema Helpers

The z object provides schema validation:

// Basic types
z.string() // String type
z.number() // Number type
z.boolean() // Boolean type

// Complex types
z.enum(['a', 'b', 'c']) // Enumeration
z.array(z.string()) // Array of strings
z.object({ key: z.string() }) // Nested object
z.optional(z.string()) // Optional field

// Modifiers
.describe('Description') // Add description
.default(value) // Set default value (makes field optional)

Agent Orchestration (src/agent.js)

The agent uses @mimik/agent-kit to orchestrate AI with tools:

const { Agent } = require('@mimik/agent-kit');

const instructions = 'You are a helpful assistant with access to MCP tools. Use them to answer user questions.';

async function* run(opt) {
const { httpPort } = global.context.info;
const { INFERENCE_API_KEY } = global.context.env;

const agent = new Agent({
name: 'MCP Assistant',
instructions,
httpClient: global.http,
mcpEndpoints: opt?.mcpEndpoints,
llm: {
endpoint: `http://127.0.0.1:${httpPort}/mimik-ai/openai/v1/chat/completions`,
apiKey: `Bearer ${INFERENCE_API_KEY}`,
}
});

const userMessage = 'find devices around me'; // Add /no_think suffix for Qwen3

const result = await agent.run(userMessage, {
toolApproval: async (toolCalls) => ({
stopAfterExecution: false, // Default: false (continue conversation)
approvals: toolCalls.map(() => true),
}),
});

for await (const event of result) {
// Forward relevant events to client
if (event.type === 'raw_model_stream_event') {
yield event.data.event;
} else if (event.type === 'tool_calls_detected') {
yield {
type: 'tool_calls_start',
tool_calls: event.data.toolCalls,
};
} else if (event.type === 'tool_results') {
yield {
type: 'tool_calls_complete',
results: event.data.results,
};
} else if (event.type === 'conversation_complete') {
yield {
type: 'done',
final_output: event.data.finalOutput,
};
break;
}
}
}

module.exports = { run };

The agent:

  1. Connects to MCP endpoints to discover available tools
  2. Sends user messages to the AI model
  3. Automatically calls tools when the AI decides to use them
  4. Streams events back to the client in real-time
Qwen3 and /no_think

The /no_think directive is specific to Qwen3 models. Qwen3 supports a "thinking" mode where it reasons through problems step-by-step. Appending /no_think to prompts disables this for faster, more direct responses. This is recommended for tool-use scenarios where you want the model to act quickly. If using other models, remove the /no_think suffix.

Streaming Events

The agent emits these event types:

Event TypeDescription
raw_model_stream_eventRaw streaming tokens from the model
tool_calls_detectedTools the model wants to call
tool_resultsResults from tool execution
conversation_completeFinal result with complete output

Advanced Configuration

For more control, you can configure the LLM settings:

const { httpPort } = global.context.info;
const { INFERENCE_API_KEY } = global.context.env;

const agent = new Agent({
name: 'MCP Assistant',
instructions,
httpClient: global.http,
mcpEndpoints: opt?.mcpEndpoints,
maxIterations: 5, // Max conversation loops (default: 5)
llm: {
endpoint: `http://127.0.0.1:${httpPort}/mimik-ai/openai/v1/chat/completions`,
apiKey: `Bearer ${INFERENCE_API_KEY}`,
model: 'qwen3-1.7b', // Qwen3 recommended for tool use
temperature: 0.1, // Lower = more deterministic
max_tokens: 2048, // Maximum response length
no_think: true // Qwen3: disable thinking mode for faster responses
}
});

// Optionally initialize explicitly to preload tools
await agent.initialize();
Dynamic Port and API Keys

Use context.info.httpPort to get the port dynamically instead of hardcoding 8083. Use separate environment variables for each API:

  • INFERENCE_API_KEY: For AI Foundation inference API
  • INSIGHT_API_KEY: For Mesh Foundation discovery API

Step 4: Build the Project

Build the mim using webpack:

npm run build

This transpiles ES6+ to ES5 (required for the Duktape runtime) and bundles everything into build/index.js.

Package as a deployable tar:

npm run package

This creates deploy/my-agent-1.0.0.tar.

Step 5: Deploy to mimOE

Deploy using the MCM API. First, get your MCM API key:

cat mimoe-api-key.env

Upload the image:

curl -X POST http://localhost:8083/mcm/v1/images \
-H "Authorization: Bearer $MCM_API_KEY" \
-F "image=@deploy/my-agent-1.0.0.tar"

Deploy the mim:

curl -X POST http://localhost:8083/mcm/v1/mims \
-H "Authorization: Bearer $MCM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "my-agent",
"image": "my-agent",
"env": {
"MCM.BASE_API_PATH": "/my-agent/v1",
"MCM.API_ALIAS": "true",
"INFERENCE_API_KEY": "1234",
"INSIGHT_API_KEY": "1234"
}
}'

Step 6: Test the AI Agent

Health Check

curl http://localhost:8083/api/my-agent/v1/healthcheck

Expected response:

{
"status": "ok",
"info": {
"httpPort": 8083,
"apiRoot": "/api/my-agent/v1"
}
}

List MCP Tools

Initialize the MCP connection and list available tools:

# Initialize
curl -X POST http://localhost:8083/api/my-agent/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "init-1",
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": { "tools": {} },
"clientInfo": { "name": "test-client", "version": "1.0.0" }
}
}'

# List tools
curl -X POST http://localhost:8083/api/my-agent/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "list-1",
"method": "tools/list"
}'

Call a Tool Directly

curl -X POST http://localhost:8083/api/my-agent/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "call-1",
"method": "tools/call",
"params": {
"name": "add",
"arguments": { "a": 5, "b": 3 }
}
}'

Expected response:

{
"jsonrpc": "2.0",
"id": "call-1",
"result": {
"content": [{ "type": "text", "text": "8" }]
}
}

Chat with the Agent

The /chat endpoint streams SSE events:

curl -X POST http://localhost:8083/api/my-agent/v1/chat \
-H "Content-Type: application/json" \
-d '{}'

You'll receive streamed events as the agent thinks, calls tools, and generates responses.

Step 7: Add Custom Tools

Extend the agent by adding your own tools in src/tools.js:

// Tool with optional parameters and defaults
server.tool('search', 'Search for items in the database', {
query: z.string().describe('Search query'),
limit: z.number().default(10).describe('Maximum results to return'),
category: z.optional(z.string()).describe('Filter by category')
}, async (args) => {
const { query, limit, category } = args;
// Implementation here
return {
content: [{ type: 'text', text: `Found results for "${query}"` }],
};
});

// Tool with enum options
server.tool('setOrderType', 'Set the order type', {
type: z.enum(['pickup', 'delivery', 'dine-in']).default('pickup').describe('Order type'),
priority: z.enum(['low', 'normal', 'high']).default('normal').describe('Priority level')
}, async (args) => {
return {
content: [{ type: 'text', text: `Order set to ${args.type} (${args.priority} priority)` }],
};
});

// Tool using persistent storage
server.tool('saveNote', 'Save a note to persistent storage', {
key: z.string().describe('Note identifier'),
value: z.string().describe('Note content')
}, async (args) => {
await global.context.storage.setItem(args.key, args.value);
return {
content: [{ type: 'text', text: `Saved note "${args.key}"` }],
};
});

// Tool that calls an external API
server.tool('getWeather', 'Get weather for a city', {
city: z.string().describe('City name')
}, async (args) => {
try {
const res = await requestAction({
url: `https://api.weather.example/v1/current?city=${args.city}`,
});
return { content: [{ type: 'text', text: res.data }] };
} catch (err) {
return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
}
});

After adding tools, rebuild and redeploy:

npm run build && npm run package
# Then redeploy via MCM API

Step 8: Advanced Security (Optional)

The generated template approves all tool calls. For production, you can add security controls.

Tool Whitelisting

Control which tools are available from each MCP server:

const agent = new Agent({
name: 'Secure Assistant',
instructions: 'Help users with controlled tool access.',
httpClient: global.http,
mcpEndpoints: [
// Simple endpoint - all tools allowed
'http://localhost:3000/mcp',

// Only allow specific tools (include mode)
{
url: 'http://localhost:3001/mcp',
options: {
toolWhitelist: ['readFile', 'listDirectory'],
whitelistMode: 'include'
}
},

// Block dangerous tools (exclude mode)
{
url: 'http://localhost:3002/mcp',
options: {
toolWhitelist: ['deleteFile', 'formatDisk'],
whitelistMode: 'exclude'
}
}
]
});

Runtime Tool Approval

Add dynamic approval logic in the toolApproval callback:

const result = await agent.run(userMessage, {
toolApproval: async (toolCalls) => ({
stopAfterExecution: false,
approvals: toolCalls.map(tool => {
// Block destructive operations
if (tool.function.name.includes('delete')) {
return { approve: false, reason: 'Destructive operations not allowed' };
}
// Approve everything else
return true;
})
})
});
Human-in-the-Loop Approval

For tools requiring human approval, implement a multi-step flow in your API handler:

  1. First request: Return { requiresApproval: true, pendingTools: toolCalls } when sensitive tools are detected
  2. UI prompts user: Client shows the pending tools and asks for confirmation
  3. Second request: Client re-sends with { approvedTools: [...] }
  4. Re-run with approvals: Agent runs again, toolApproval callback checks against the pre-approved list

This pattern keeps humans in control of sensitive operations while maintaining the agentic flow.

Recap: The Four Things You Implemented

This tutorial walked you through implementing the four pieces of an AI Agent mim (as explained in How AI Agent mims Work):

ConfigFileWhat You Did
API Handlersrc/index.jsSet up /chat, /mcp, /healthcheck endpoints with SSE streaming
Instructionssrc/agent.jsDefined agent behavior in natural language
MCP Endpointssrc/tools.jsCreated tools with @mimik/mcp-kit and exposed via /mcp
Context Retrieval(optional)Use mkb or edis for RAG and secured data (not covered in this basic tutorial)

Serverless Execution

Mims are stateless. Each request instantiates a fresh execution context. Use context.storage for persistence between requests.

Troubleshooting

Build Fails with Syntax Errors

The Duktape runtime only supports ES5. Ensure webpack is transpiling correctly:

# Check webpack.config.js targets ES5
npm run build -- --mode development

Tool Not Found

Ensure the tool is registered before the mim is packaged:

  1. Check tools.js exports the server
  2. Rebuild: npm run build && npm run package
  3. Redeploy the mim

Agent Returns Empty Response

Check that:

  1. A model is loaded in mimOE (GET /mimik-ai/openai/v1/models)
  2. The httpClient is passed correctly to the Agent (global.http)
  3. MCP endpoints are correctly configured in getMcpEndpoints()
  4. The mim is deployed with correct environment variables (INFERENCE_API_KEY, INSIGHT_API_KEY)

Next Steps