Skip to content
Draft
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
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
],
"workspaces": {
"packages/appkit": {
"ignoreDependencies": ["ai", "@langchain/core", "zod"],
"ignoreDependencies": ["ai", "@langchain/core"],
"entry": ["src/agents/*.ts"]
},
"packages/appkit-ui": {}
Expand Down
12 changes: 4 additions & 8 deletions packages/appkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,19 @@
"semver": "7.7.3",
"shared": "workspace:*",
"vite": "npm:rolldown-vite@7.1.14",
"ws": "8.18.3"
"ws": "8.18.3",
"zod": "^4.0.0"
},
"peerDependencies": {
"@langchain/core": ">=0.3.0",
"ai": ">=4.0.0",
"zod": ">=3.0.0"
"ai": ">=4.0.0"
},
"peerDependenciesMeta": {
"ai": {
"optional": true
},
"@langchain/core": {
"optional": true
},
"zod": {
"optional": true
}
},
"devDependencies": {
Expand All @@ -115,8 +112,7 @@
"@types/pg": "8.16.0",
"@types/ws": "8.18.1",
"@vitejs/plugin-react": "5.1.1",
"ai": "7.0.0-beta.76",
"zod": "^4.3.6"
"ai": "7.0.0-beta.76"
},
"overrides": {
"vite": "npm:rolldown-vite@7.1.14"
Expand Down
7 changes: 7 additions & 0 deletions packages/appkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ export {
toPlugin,
} from "./plugin";
export { analytics, files, genie, lakebase, server, serving } from "./plugins";
export {
type FunctionTool,
type HostedTool,
mcpServer,
type ToolConfig,
tool,
} from "./plugins/agent/tools";
export type {
EndpointConfig,
ServingEndpointEntry,
Expand Down
110 changes: 110 additions & 0 deletions packages/appkit/src/plugins/agent/tests/function-tool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, expect, test } from "vitest";
import {
functionToolToDefinition,
isFunctionTool,
} from "../tools/function-tool";

describe("isFunctionTool", () => {
test("returns true for valid FunctionTool", () => {
expect(
isFunctionTool({
type: "function",
name: "greet",
execute: async () => "hello",
}),
).toBe(true);
});

test("returns true for minimal FunctionTool", () => {
expect(
isFunctionTool({
type: "function",
name: "x",
execute: () => "y",
}),
).toBe(true);
});

test("returns false for null", () => {
expect(isFunctionTool(null)).toBe(false);
});

test("returns false for non-object", () => {
expect(isFunctionTool("function")).toBe(false);
});

test("returns false for wrong type", () => {
expect(
isFunctionTool({
type: "genie-space",
name: "x",
execute: () => "y",
}),
).toBe(false);
});

test("returns false when execute is missing", () => {
expect(isFunctionTool({ type: "function", name: "x" })).toBe(false);
});

test("returns false when name is missing", () => {
expect(isFunctionTool({ type: "function", execute: () => "y" })).toBe(
false,
);
});
});

describe("functionToolToDefinition", () => {
test("converts a FunctionTool with all fields", () => {
const def = functionToolToDefinition({
type: "function",
name: "getWeather",
description: "Get current weather",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async () => "sunny",
});

expect(def.name).toBe("getWeather");
expect(def.description).toBe("Get current weather");
expect(def.parameters).toEqual({
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
});
});

test("uses name as fallback description", () => {
const def = functionToolToDefinition({
type: "function",
name: "myTool",
execute: async () => "result",
});

expect(def.description).toBe("myTool");
});

test("uses empty object schema when parameters are null", () => {
const def = functionToolToDefinition({
type: "function",
name: "noParams",
parameters: null,
execute: async () => "ok",
});

expect(def.parameters).toEqual({ type: "object", properties: {} });
});

test("uses empty object schema when parameters are omitted", () => {
const def = functionToolToDefinition({
type: "function",
name: "noParams",
execute: async () => "ok",
});

expect(def.parameters).toEqual({ type: "object", properties: {} });
});
});
131 changes: 131 additions & 0 deletions packages/appkit/src/plugins/agent/tests/hosted-tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, expect, test } from "vitest";
import { isHostedTool, resolveHostedTools } from "../tools/hosted-tools";

describe("isHostedTool", () => {
test("returns true for genie-space", () => {
expect(
isHostedTool({ type: "genie-space", genie_space: { id: "abc" } }),
).toBe(true);
});

test("returns true for vector_search_index", () => {
expect(
isHostedTool({
type: "vector_search_index",
vector_search_index: { name: "cat.schema.idx" },
}),
).toBe(true);
});

test("returns true for custom_mcp_server", () => {
expect(
isHostedTool({
type: "custom_mcp_server",
custom_mcp_server: { app_name: "my-app", app_url: "my-app-url" },
}),
).toBe(true);
});

test("returns true for external_mcp_server", () => {
expect(
isHostedTool({
type: "external_mcp_server",
external_mcp_server: { connection_name: "conn1" },
}),
).toBe(true);
});

test("returns false for FunctionTool", () => {
expect(
isHostedTool({ type: "function", name: "x", execute: () => "y" }),
).toBe(false);
});

test("returns false for null", () => {
expect(isHostedTool(null)).toBe(false);
});

test("returns false for unknown type", () => {
expect(isHostedTool({ type: "unknown" })).toBe(false);
});

test("returns false for non-object", () => {
expect(isHostedTool(42)).toBe(false);
});
});

describe("resolveHostedTools", () => {
test("resolves genie-space to correct MCP endpoint", () => {
const configs = resolveHostedTools([
{ type: "genie-space", genie_space: { id: "space123" } },
]);

expect(configs).toHaveLength(1);
expect(configs[0].name).toBe("genie-space123");
expect(configs[0].url).toBe("/api/2.0/mcp/genie/space123");
});

test("resolves vector_search_index with 3-part name", () => {
const configs = resolveHostedTools([
{
type: "vector_search_index",
vector_search_index: { name: "catalog.schema.my_index" },
},
]);

expect(configs).toHaveLength(1);
expect(configs[0].name).toBe("vs-catalog-schema-my_index");
expect(configs[0].url).toBe(
"/api/2.0/mcp/vector-search/catalog/schema/my_index",
);
});

test("throws for invalid vector_search_index name", () => {
expect(() =>
resolveHostedTools([
{
type: "vector_search_index",
vector_search_index: { name: "bad.name" },
},
]),
).toThrow("3-part dotted");
});

test("resolves custom_mcp_server", () => {
const configs = resolveHostedTools([
{
type: "custom_mcp_server",
custom_mcp_server: { app_name: "my-app", app_url: "my-app-endpoint" },
},
]);

expect(configs[0].name).toBe("my-app");
expect(configs[0].url).toBe("my-app-endpoint");
});

test("resolves external_mcp_server", () => {
const configs = resolveHostedTools([
{
type: "external_mcp_server",
external_mcp_server: { connection_name: "conn1" },
},
]);

expect(configs[0].name).toBe("conn1");
expect(configs[0].url).toBe("/api/2.0/mcp/external/conn1");
});

test("resolves multiple tools preserving order", () => {
const configs = resolveHostedTools([
{ type: "genie-space", genie_space: { id: "g1" } },
{
type: "external_mcp_server",
external_mcp_server: { connection_name: "e1" },
},
]);

expect(configs).toHaveLength(2);
expect(configs[0].name).toBe("genie-g1");
expect(configs[1].name).toBe("e1");
});
});
34 changes: 34 additions & 0 deletions packages/appkit/src/plugins/agent/tests/mcp-server-helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from "vitest";
import {
isHostedTool,
mcpServer,
resolveHostedTools,
} from "../tools/hosted-tools";

describe("mcpServer()", () => {
test("returns a CustomMcpServerTool with correct shape", () => {
const result = mcpServer("my-app", "https://example.com/mcp");

expect(result).toEqual({
type: "custom_mcp_server",
custom_mcp_server: {
app_name: "my-app",
app_url: "https://example.com/mcp",
},
});
});

test("isHostedTool recognizes mcpServer() output", () => {
expect(isHostedTool(mcpServer("x", "y"))).toBe(true);
});

test("resolveHostedTools resolves mcpServer() output to an endpoint config", () => {
const configs = resolveHostedTools([
mcpServer("vector-search", "https://host/mcp/vs"),
]);

expect(configs).toHaveLength(1);
expect(configs[0].name).toBe("vector-search");
expect(configs[0].url).toBe("https://host/mcp/vs");
});
});
Loading