Codecall
An Open Source Typescript implementation of Programmatic Tool Calling for AI Agents.
Codecall changes how agents interact with tools by letting them write and execute code (in secured sandboxes) that orchestrates multiple tools programmatically (like an API) to do a task, rather than making individual tool calls that bloat the context and increase the token usage like in traditional agents. Codecall also has progressive tool discovery & error handling!
Demo →
Codecall vs a Traditional Agent performing the same task w/ the same tools
74.7% fewer tokens · 92.3% fewer tool calls
https://github.com/user-attachments/assets/6cfc44e4-784a-4258-97d6-6ae694ef4024
Problems w/ Traditional Agents
Traditional tool calling in agents has many fundamental architectural issues that get worse at scale:
1. Context Bloat & Wasting Tokens
Traditional agents send EVERY tool definition with every request, so for 20 tools thats about 6k+ tokens of schema definitions in every inference call, even for questions like, "what can you do?" or "update the date for this" where they are not necessary. This issue scales with tool count and gets multiplied with every turn in a conversation.
2. N Inference Calls for N Tool Operations
Every tool operation requires a full inference round-trip, so "Delete all completed tasks" becomes: LLM calls findTasks, waits, calls deleteTask for task 1, waits, calls for task 2... Each call resends the entire conversation history including all previous tool results, so the tokens compound. For example those 20 steps = 20+ inference calls with exponentially growing context.
3. No Parallel Execution
Similar to #2, but traditional agents execute tools sequentially even when operations are independent. Ten API calls that could run simultaneously instead happen one at a time with the agent reasoning between each of them that wastes time, tokens, and unnecessarily sends info into the context window and through the API provider's infrastructure.
4. Models are not great at Lookup
Benchmarks show models have a 10-50% failure rate when searching through large datasets in context. They hallucinate field names, miss entries, and get confused by similar data.
But doing this programmatically fixes this because it can just write code, as its deterministic so 0% failure rate
users.filter((u) => u.role === "admin");
Codecall's Approach
Let models do what they're good at: writing code.
LLMs have enormous amounts of real-world TypeScript in their training data. They're significantly better at writing code to call APIs than they are at the arbitrary JSON matching that tool calling requires.
Codecall ALSO only has 2 tools: readFile and executeCode, along with our SDK file tree of your tools (that we generate ahead of time) in context, so the agent only reads and gets the context it needs as needed based on the task, so this makes a 30 tool setup effectively have the same base context as a 5 tool setup (only the file tree gets larger)
When code in the sandbox calls tools.namespace.method() a Proxy intercepts it, sends a JSON message via stdout to the host process, which routes it through ToolRegistry to execute the actual tool, then sends the result back via stdin where the sandbox resolves the waiting Promise, making IPC look like normal async function calls.
// Instead of 20+ inference passes and 90k+ tokens:
const allUsers = await tools.users.listAllUsers();
const adminUsers = allUsers.filter((u) => u.role === "admin");
const resources = await tools.resources.getSensitiveResources();
progress({
step: "Data loaded",
admins: adminUsers.length,
resources: resources.length,
});
const revokedAccesses = [];
const failedAccesses = [];
for (const admin of adminUsers) {
for (const resource of resources) {
try {
const result = await tools.permissions.revokeAccess({
userId: admin.id,
resourceId: resource.id,
});
if (result.success) {
revokedAccesses.push({ admin: admin.name, resource: resource.name });
}
} catch (err) {
failedAccesses.push({
admin: admin.name,
resource: resource.name,
error: err.message,
});
}
}
}
return {
totalAdmins: adminUsers.length,
resourcesAffected: resources.length,
accessesRevoked: revokedAccesses.length,
accessesFailed: failedAccesses.length,
};
Three inference passes. The code runs in a sandbox calling all 20 updates programmatically with step by step updates, only pulling the relevant context when it is needed, and returning only what is necessary. Saving tens of thousands worth of tokens, and doing everything more efficiently.
Getting Started
Prerequisites
- Node.js v20+ and npm
- Deno v2+ (for sandbox execution) - Install Deno
- OpenRouter API Key (or any OpenRouter compatible provider)
Clone the repo
git clone https://github.com/zeke-john/codecall.git
cd codecall
npm install
Environment Variables
Create a .env file in the project root:
OPENROUTER_API_KEY=your_openrouter_api_key
Optional variables:
TODOIST_API_KEY=your_todoist_api_key # For Todoist the todoist MCP
MCP_PORT=4001 # Custom port for demo MCP server
Running the Demo MCP Server
Codecall includes a demo MCP server with user management tools for testing. Start it in a separate terminal:
npm run mcp
This starts an HTTP MCP server at http://localhost:4001/mcp with 18 user management tools (create, update, delete, search, etc.) that operate on demoMCP/users.json.
Running the Codecall Agent
With the demo MCP server running:
npm run codecall
This starts an interactive chat session using the Codecall approach (2 internal tools + SDK file tree). The agent will read SDK files on-demand and write code to execute tasks in a sandbox.
Running the Traditional Agent
For comparison, run the traditional agent that exposes all tools directly:
npm run traditional
Chat Commands
Both agents support these commands:
/exit or /quit - Exit the chat
/clear - Clear conversation history
/tools - List all available tools
Connecting to Custom MCP Servers
Option 1: Command Line Arguments
Connect to stdio-based MCP servers:
npm run codecall -- --stdio namespace npx @some/mcp-server arg1 arg2
Connect to HTTP-based MCP servers:
npm run codecall -- --http namespace http://localhost:3000/mcp
Multiple servers:
npm run codecall -- \
--stdio todoist npx @doist/todoist-ai \
--http demo http://localhost:4001/mcp
Option 2: Modify Default Servers
Edit scripts/chat.ts and modify the getDefaultServers() function:
function getDefaultServers(): MCPServerEntry[] {
return [
{
namespace: "demo",
config: {
type: "http",
url: "http://localhost:4001/mcp",
},
},
{
namespace: "github",
config: {
type: "stdio",
command: "docker",
args: ["run", "-i", "--rm", "ghcr.io/github/github-mcp-server"],
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN },
},
},
];
}
Generating SDK Files from MCP Servers
Before using Codecall with a new MCP server, generate SDK files:
# From an HTTP MCP server
npm run test:mcp -- http http://localhost:4001/mcp
# From a stdio MCP server
npm run test:mcp -- stdio npx @doist/todoist-ai
# With environment variables
npm run test:mcp -- stdio npx @some/server --env API_KEY=xxx
# Custom output directory
npm run test:mcp -- http http://localhost:4001/mcp --output ./mysdks
SDK files are written to generatedSdks/tools/{namespace}/. see docs/exampleSdkFile.ts or any of the existing SDK files for the recommended format.
Dev Scripts
For Observability & testing individual components:
# Debugs the demo MCP server using the MCP Inspector
npm run inspect
# Test the sandbox execution
npx tsx scripts/testSandbox.ts
# Test the tool registry
npx tsx scripts/testToolRegistry.ts
How Codecall Works
Codecall gives the model 2 tools + a file tree to work with so the model still controls the entire flow that decides what to read, what code to write, when to execute, and how to respond... so everything stays fully agentic.
Instead of exposing every tool directly to the LLM for it to call, Codecall:
- Converts your MCP definitions into TypeScript SDK files (types + function signatures)
- Shows the model a directory tree of available files
- Allows the model to selectively read SDK files to understand types and APIs
- Lets the model write code to accomplish the task
- Executes that code in a deno sandbox with access to your actual tools as functions
- Returns the execution result back (success/error)
- Lets the model produce a respond or continue
The system message by default has an SDK file tree showing all available tools as files, it just shows them the file tree and not the actual contents of each of the files, so it can progressively discover tools as it needs them for a certain task
Example:
tools/
├─ Database
│ ├─ checkEmailExists.ts
│ ├─ cloneUser.ts
│ ├─ createUser.ts
│ ├─ deactivateUsersByDomain.ts
│ ├─ deleteUser.ts
│ ├─ getUsersByFavoriteColor.ts
│ ├─ getUsersCreatedAfter.ts
│ ├─ getUserStats.ts
│ ├─ searchUsers.ts
│ ├─ setUserActiveStatus.ts
│ ├─ setUserFavoriteColor.ts
│ ├─ updateUser.ts
│ └─ validateEmailFormat.ts
└─ todoist
├─ addComments.ts
├─ addProjects.ts
├─ addSections.ts
├─ addTasks.ts
├─ manageAssignments.ts
├─ search.ts
├─ updateComments.ts
├─ updateProjects.ts
├─ updateSections.ts
├─ updateTasks.ts
└─ userInfo.ts
1. readFile(path: string)
Returns the full contents of a specific SDK file, including type definitions, function signatures, and schemas.
Example:
readFile({ path: "tools/users/listAllUsers.ts" }); ->
/**
* HOW TO CALL THIS TOOL:
* await tools.users.listAllUsers({ limit: 100, offset: 0 })
*
* This is the ONLY way to invoke this tool in your code.
*/
export interface ListAllUsersInput {
limit?: number;
offset?: number;
}
export interface User {
id: string;
name: string;
email: string;
role: "admin" | "user" | "guest";
department: string;
createdAt: string;
}
export async function listAllUsers(input: ListAllUsersInput): Promise<User[]>;
2. executeCode(code: string)
Executes TypeScript code in a Deno sandbox. Returns either the successful output or an error w/ the execution trace.
Example:
executeCode(`
const users = await tools.users.listAllUsers({ limit: 100 });
return users.filter(u => u.role === "admin");
`);
Success returns:
{
status: "success",
output: [
{ id: "1", name: "Alice", role: "admin", ... },
{ id: "2", name: "Bob", role: "admin", ... }
],
progressLogs: [{ step: "Loading users..." }]
}
Error returns:
{
status: "error",
error: `=== ERROR ===
Type: Error
Message: Undefined value at 'result[0]'. This usually means you accessed a property that doesn't exist.
=== STACK TRACE ===
Error: Undefined value at 'result[0]'...
at validateResult (file:///.../sandbox.ts:68:11)
at file:///.../sandbox.ts:99:5
progressLogs: [{ step: "Loading users..." }]
}
The error includes the full stack trace, giving the model maximum context to fix the issue and try again, then update the SDK file once fixed.
Code Execution & Sandboxing
When the model calls executeCode(), Codecall runs that code inside a fresh, short-lived Deno sandbox. Each sandbox is spun up using Deno and runs the code in isolation. Deno’s security model