feat(plugin): flat tool draft with namespace

Effect draft.add accepts flat tool objects matching Promise ergonomics.
Migrate the tool grouping concept fully to namespace: rename MCP's group
helper/registration to namespace and thread namespace through the flat
Effect and Promise draft declarations.
This commit is contained in:
Aiden Cline 2026-07-12 15:30:27 -05:00
commit 6714f74433
13 changed files with 157 additions and 88 deletions

View file

@ -123,6 +123,17 @@ export function make(config: Config<any, any, any> | DynamicConfig): AnyTool {
return makeTyped(config)
}
/**
* Split a flat tool declaration into its registration name, opaque Tool value,
* and registration options. Narrowing on `jsonSchema` selects the matching
* constructor, so no cast is needed to build the Tool from the flat union.
*/
export function fromFlat(flat: FlatDefinition<any, any, any> | FlatDynamicDefinition) {
const { name, namespace, codemode, ...config } = flat
const tool = "jsonSchema" in config ? makeDynamic(config) : makeTyped(config)
return { name, tool, options: { namespace, codemode } satisfies RegisterOptions }
}
function makeTyped<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
@ -212,14 +223,14 @@ export const validateName = (name: string) =>
? Effect.void
: Effect.fail(new RegistrationError({ name, message: `Invalid tool name: ${name}` }))
export const registrationEntries = (tools: Readonly<Record<string, AnyTool>>, group?: string) =>
export const registrationEntries = (tools: Readonly<Record<string, AnyTool>>, namespace?: string) =>
Object.entries(tools).map(([name, tool]) => {
const normalized = name.replace(/[^a-zA-Z0-9_-]/g, "_")
const parent = group?.replace(/[^a-zA-Z0-9_-]/g, "_")
const parent = namespace?.replace(/[^a-zA-Z0-9_-]/g, "_")
return {
key: parent === undefined ? normalized : `${parent}_${normalized}`,
name: normalized,
group: parent,
namespace: parent,
tool,
}
})
@ -271,15 +282,68 @@ export interface ToolExecuteAfterEvent {
}
export interface RegisterOptions {
readonly group?: string
/** Dotted CodeMode path prefix, e.g. "slack.admin". */
readonly namespace?: string
/** Defaults to true. False exposes the tool directly to the provider. */
readonly codemode?: boolean
}
export type FlatDefinition<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
Structured extends SchemaType<any> = Output,
> = {
readonly name: string
readonly namespace?: string
readonly codemode?: boolean
readonly description: string
readonly input: Input
readonly output: Output
readonly structured?: Structured
readonly toStructuredOutput?: Config<Input, Output, Structured>["toStructuredOutput"]
readonly execute: Config<Input, Output, Structured>["execute"]
readonly toModelOutput?: Config<Input, Output, Structured>["toModelOutput"]
}
export type FlatDynamicDefinition = {
readonly name: string
readonly namespace?: string
readonly codemode?: boolean
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute: DynamicConfig["execute"]
}
export interface ToolDraft {
add<
Input extends SchemaType<any>,
Output extends SchemaType<any>,
Structured extends SchemaType<any> = Output,
>(tool: FlatDefinition<Input, Output, Structured>): void
add(tool: FlatDynamicDefinition): void
add(name: string, tool: AnyTool, options?: RegisterOptions): void
}
export type Registration = {
readonly name: string
readonly tool: AnyTool
readonly options?: RegisterOptions
}
/** Run a draft callback and collect the tools it declared, in registration order. */
export function fromDraft(callback: (draft: ToolDraft) => void) {
const registrations: Array<Registration> = []
const add = (
nameOrTool: string | FlatDefinition<any, any, any> | FlatDynamicDefinition,
tool?: AnyTool,
options?: RegisterOptions,
) =>
registrations.push(typeof nameOrTool === "string" ? { name: nameOrTool, tool: tool!, options } : fromFlat(nameOrTool))
callback({ add })
return registrations
}
export interface ToolHooks {
readonly "execute.before": ToolExecuteBeforeEvent
readonly "execute.after": ToolExecuteAfterEvent

View file

@ -16,7 +16,8 @@ export type Definition<
Structured extends SchemaType<any> = Output,
> = {
readonly name: string
readonly options?: RegisterOptions
readonly namespace?: string
readonly codemode?: boolean
readonly description: string
readonly input: Input
readonly output: Output
@ -37,7 +38,8 @@ export type Definition<
export type DynamicDefinition = {
readonly name: string
readonly options?: RegisterOptions
readonly namespace?: string
readonly codemode?: boolean
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
@ -67,12 +69,6 @@ export interface ToolExecuteAfterEvent {
outputPaths?: ReadonlyArray<string>
}
export interface RegisterOptions {
readonly group?: string
/** Defaults to true. False exposes the tool directly to the provider. */
readonly codemode?: boolean
}
export interface ToolDraft {
add<
Input extends SchemaType<any>,