Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/codemode-dynamic-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/codemode": patch
---

Add a `dynamicTools()` helper for runtime-resolved codemode providers, support dotted provider paths, and allow provider prompt documentation (`types`) to be loaded asynchronously.
11 changes: 11 additions & 0 deletions packages/codemode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"@playwright/test": "^1.59.1",
"@tanstack/ai": "^0.11.0",
"ai": "^6.0.168",
"oxfmt": "^0.46.0",
"tsdown": "^0.21.9",
"tsx": "^4.21.0",
"typescript": "^6.0.3",
"vitest": "4.1.4",
"zod": "^4.3.6"
},
Expand Down Expand Up @@ -59,6 +63,11 @@
"import": "./dist/mcp.js",
"require": "./dist/mcp.js"
},
"./dynamic": {
"types": "./dist/dynamic.d.ts",
"import": "./dist/dynamic.js",
"require": "./dist/dynamic.js"
},
"./tanstack-ai": {
"types": "./dist/tanstack-ai.d.ts",
"import": "./dist/tanstack-ai.js",
Expand All @@ -67,6 +76,8 @@
},
"scripts": {
"build": "tsx ./scripts/build.ts",
"build:standalone": "tsx ./scripts/build.ts --tsconfig ./tsconfig.standalone.json",
"prepare": "npm run build:standalone",
"test": "vitest run",
"test:e2e": "npx playwright test --config e2e/playwright.config.ts",
"test:watch": "vitest"
Expand Down
13 changes: 12 additions & 1 deletion packages/codemode/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { execSync } from "node:child_process";
import { build } from "tsdown";

const tsconfig = process.argv.includes("--tsconfig")
? process.argv[process.argv.indexOf("--tsconfig") + 1]
: undefined;

async function main() {
await build({
clean: true,
dts: true,
entry: ["src/index.ts", "src/ai.ts", "src/mcp.ts", "src/tanstack-ai.ts"],
entry: [
"src/index.ts",
"src/ai.ts",
"src/mcp.ts",
"src/dynamic.ts",
"src/tanstack-ai.ts"
],
tsconfig,
deps: {
skipNodeModulesBundle: true,
neverBundle: ["cloudflare:workers"]
Expand Down
81 changes: 81 additions & 0 deletions packages/codemode/src/dynamic-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ToolProvider, ToolProviderTypes } from "./executor";

/**
* A dynamic tool provider is the escape hatch for integrations that do not
* know their full tool surface ahead of time, or deliberately do not want to
* pay the cost of discovering it up front.
*
* Instead of contributing a static `tools` record, a dynamic provider accepts
* arbitrary dotted subpaths at runtime and decides for itself whether they are
* valid. In other words, if sandbox code evaluates
* `mcp.someServer.files.read({ path: "/tmp/x" })`, codemode forwards the
* trailing path `"files.read"` and the raw argument array to `callTool()`.
*
* The `types` field name is inherited from existing codemode providers, but for
* dynamic providers it should be read much more literally as *LLM-facing
* documentation*. Codemode does not parse, typecheck, or validate this text as
* TypeScript. The string is inserted verbatim into the prompt block shown to
* the model. That means it can contain declaration-like examples, prose API
* notes, usage conventions, or any other guidance that helps the model produce
* sensible calls.
*
* We intentionally keep this field synchronous in the minimal dynamic-provider
* design. Tool descriptions are assembled eagerly by the surrounding codemode
* integrations, and trying to sneak lazy/async prompt material through that
* path made the implementation much more invasive than the runtime feature
* justified. If we ever want remote discovery-backed docs, that should likely
* be a separate API with its own explicit lifecycle.
*/
export interface DynamicToolsOptions {
/**
* Namespace path exposed in sandbox code.
*
* Dotted names are allowed. For example, `name: "mcp.someServer"` makes the
* provider reachable as `mcp.someServer.*` in generated code.
*/
name?: string;

/**
* Runtime handler for tool calls under this namespace.
*
* Codemode forwards the full dotted subpath below `name` verbatim. If the
* model attempts `mcp.someServer.foo.bar(1, 2)`, this function receives
* `name === "foo.bar"` and `args === [1, 2]`.
*/
callTool: (name: string, args: unknown[]) => Promise<unknown>;

/**
* Optional model-facing documentation inserted into the codemode prompt.
*
* Despite the legacy field name, this does not need to be valid TypeScript.
* It is best thought of as provider documentation for the LLM: declaration
* snippets, examples, conventions, caveats, etc.
*/
types?: ToolProviderTypes;

/**
* Marks this provider as preferring positional-argument examples.
*
* The runtime hook always receives the raw argument array regardless; this
* flag only affects how surrounding codemode integrations think about the
* provider surface.
*/
positionalArgs?: boolean;
}

/**
* Construct a codemode provider whose tool surface is resolved dynamically at
* runtime instead of from a static `tools` record.
*
* The helper exists so we can keep the public API crisp and intentional. Users
* opt into the dynamic behavior explicitly instead of smuggling it through the
* generic `ToolProvider` shape.
*/
export function dynamicTools(options: DynamicToolsOptions): ToolProvider {
return {
name: options.name,
callTool: options.callTool,
types: options.types,
positionalArgs: options.positionalArgs
};
}
1 change: 1 addition & 0 deletions packages/codemode/src/dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { dynamicTools, type DynamicToolsOptions } from "./dynamic-tools";
Loading
Loading