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
117 changes: 117 additions & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, it, expect } from "vitest";
import { getHost, getPort, jsonSchemaToTypebox } from "./index.js";
import { Type, Kind, OptionalKind } from "@sinclair/typebox";

describe("getHost", () => {
it("returns default host when no config", () => {
expect(getHost()).toBe("127.0.0.1");
expect(getHost(undefined)).toBe("127.0.0.1");
});

it("returns default host when config has no mcpHost", () => {
expect(getHost({})).toBe("127.0.0.1");
expect(getHost({ mcpHost: 123 })).toBe("127.0.0.1");
expect(getHost({ mcpHost: "" })).toBe("127.0.0.1");
});

it("returns custom host from config", () => {
expect(getHost({ mcpHost: "192.168.1.100" })).toBe("192.168.1.100");
});
});

describe("getPort", () => {
it("returns default port when no config", () => {
expect(getPort()).toBe(9990);
expect(getPort(undefined)).toBe(9990);
});

it("returns default port when config has no mcpPort", () => {
expect(getPort({})).toBe(9990);
expect(getPort({ mcpPort: "9991" })).toBe(9990);
});

it("returns custom port from config", () => {
expect(getPort({ mcpPort: 8080 })).toBe(8080);
});
});

describe("jsonSchemaToTypebox", () => {
it("returns empty object schema for undefined input", () => {
const schema = jsonSchemaToTypebox(undefined);
expect(schema[Kind]).toBe("Object");
expect(schema.properties).toEqual({});
});

it("returns empty object schema for schema without properties", () => {
const schema = jsonSchemaToTypebox({ type: "object" });
expect(schema.properties).toEqual({});
});

it("converts string property", () => {
const schema = jsonSchemaToTypebox({
properties: { name: { type: "string", description: "A name" } },
required: ["name"],
});
expect(schema.properties).toHaveProperty("name");
expect(schema.properties.name[Kind]).toBe("String");
expect(schema.properties.name.description).toBe("A name");
});

it("converts number and integer properties", () => {
const schema = jsonSchemaToTypebox({
properties: {
speed: { type: "number" },
count: { type: "integer" },
},
required: ["speed", "count"],
});
expect(schema.properties.speed[Kind]).toBe("Number");
expect(schema.properties.count[Kind]).toBe("Number");
});

it("converts boolean property", () => {
const schema = jsonSchemaToTypebox({
properties: { enabled: { type: "boolean" } },
required: ["enabled"],
});
expect(schema.properties.enabled[Kind]).toBe("Boolean");
});

it("marks properties not in required array as optional", () => {
const schema = jsonSchemaToTypebox({
properties: {
req: { type: "string" },
opt: { type: "string" },
},
required: ["req"],
});
// Required property has no Optional symbol
expect(schema.properties.req[OptionalKind]).toBeUndefined();
// Non-required property has Optional symbol
expect(schema.properties.opt[OptionalKind]).toBe("Optional");
});

it("converts array property", () => {
const schema = jsonSchemaToTypebox({
properties: { items: { type: "array" } },
required: ["items"],
});
expect(schema.properties.items[Kind]).toBe("Array");
});

it("converts object property to Record", () => {
const schema = jsonSchemaToTypebox({
properties: { metadata: { type: "object" } },
required: ["metadata"],
});
expect(schema.properties.metadata[Kind]).toBe("Record");
});

it("defaults unknown types to string", () => {
const schema = jsonSchemaToTypebox({
properties: { unknown: { type: "foobar" } },
required: ["unknown"],
});
expect(schema.properties.unknown[Kind]).toBe("String");
});
});
6 changes: 3 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@ interface McpToolDef {
inputSchema: Record<string, unknown>;
}

function getHost(pluginConfig?: Record<string, unknown>): string {
export function getHost(pluginConfig?: Record<string, unknown>): string {
if (pluginConfig && typeof pluginConfig.mcpHost === "string" && pluginConfig.mcpHost) {
return pluginConfig.mcpHost;
}
return DEFAULT_HOST;
}

function getPort(pluginConfig?: Record<string, unknown>): number {
export function getPort(pluginConfig?: Record<string, unknown>): number {
if (pluginConfig && typeof pluginConfig.mcpPort === "number") {
return pluginConfig.mcpPort;
}
return DEFAULT_PORT;
}

/** Convert a JSON Schema properties object into a TypeBox Type.Object schema. */
function jsonSchemaToTypebox(
export function jsonSchemaToTypebox(
inputSchema?: Record<string, unknown>,
): ReturnType<typeof Type.Object> {
if (!inputSchema) {
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
"description": "Exposes dimos MCP tools to the OpenClaw agent",
"type": "module",
"dependencies": {},
"devDependencies": {},
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"vitest": "^3.0.0",
"@sinclair/typebox": "^0.34.0"
},
"peerDependencies": {
"openclaw": ">=2026.1.26"
},
Expand Down