@zapier/toolport
v0.3.0
Published
Type-safe RPC communication layer for iframe-based tool execution with Zod schema validation
Downloads
38
Maintainers
Readme
@zapier/toolport
ToolPort is a TypeScript toolkit that provides a structured approach to inter-component communication and JSON schema management in your applications. It focuses on clarity and maintainability by offering utilities to define and serialize remote tools with well-typed input and output schemas, and to generate TypeScript stubs for type-safe invocations.
Key features include:
- Definition of remote tools with clear and type-safe interfaces.
- Conversion between Zod schemas and JSON Schemas.
- Generation of TypeScript interfaces for seamless AI prompting.
- Built-in support for parent-child iframe communication.
Installation
pnpm add @zapier/toolportPeer dependencies:
zod(required)prettier(optional, for formatted TypeScript stub generation)
Development
# Install dependencies
pnpm install
# Run tests
pnpm test
# Run tests in watch mode
pnpm test:watch
# Build the package
pnpm buildUsage
Here's the basic setup:
// Define your tools
const tools = [
defineTool({
name: "getData",
parameters: z.object({ id: z.number() }),
returns: z.object({ data: z.string() }),
execute: async ({ id }) => {
const data = await fetchSomeData(id);
return { data };
},
}),
];Here's the parent component:
// Parent component that hosts an iframe
const ParentComponent = () => {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
if (!iframeRef.current) return;
// Set up server to handle requests from iframe
const cleanup = createPostMessageServerTransport({
tools, // This is from your tools definition above.
targetWindow: iframeRef.current.contentWindow!,
listenWindow: window,
});
return cleanup;
}, []);
return <iframe ref={iframeRef} src="/child" />;
};Prepping for child and code generation:
// Serialize your tools as JSON schema
const serializedTools = serializeTools(tools);
// Build Typescript stubs for your serialized tools
const stubs = generateTypeScriptStubs(serializedTools, "FancyClient");
// Example: Using the generated stubs with an AI
const ai = new ChatGPT(); // or your AI client
const prompt = 'Get the data for 123';
// The AI can use the typed stubs to understand the available tools
const code = await ai.chat(`
You are a brilliant engineer who can write React code.
You have access to these tools under the FancyClient object:
${stubs}
Export a single React component called Widget. Use tailwind.
`);
// Code looks like `const { data } = await FancyClient.getData({ id: 123 });`!
// You should be able to lint and typecheck this as well.Here's the child:
// Create the client to call parent's tools
const transport = createPostMessageClientTransport({
remoteWindow: window.parent,
listenWindow: window,
});
const FancyClient = createClientFromSerialized(serializedTools, transport);
// When done, clean up the transport to avoid memory leaks
// transport.cleanup();
// Child component inside iframe
const ChildComponent = () => {
const [data, setData] = useState<string>();
useEffect(() => {
// Call parent's tool
const fetchData = async () => {
const result = await FancyClient.getData({ id: 123 });
setData(result.data);
};
fetchData();
}, []);
return <div>{data}</div>;
};You might want something more like this if integrating into a live runner:
export const CustomCodeWidget = ({
generatedCode,
serializedTools,
}: {
generatedCode: string;
serializedTools: SerializedTool[];
}) => {
const srcDoc = `
<!DOCTYPE html>
<html>
<head>
<script type="importmap">{
"imports": {
"@jsxImportSource": "https://esm.sh/[email protected]",
"react-dom/client": "https://esm.sh/[email protected]/client",
"react": "https://esm.sh/[email protected]",
"@zapier/toolport": "https://esm.sh/@zapier/toolport"
}
}</script>
<script type="module" src="https://esm.sh/run"></script>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// Set up the FancyClient to allow calls to parent's tools.
import { createClientFromSerialized, createPostMessageClientTransport } from "@zapier/toolport";
const FancyClient = createClientFromSerialized(
${JSON.stringify(serializedTools)},
createPostMessageClientTransport({
remoteWindow: window.parent,
listenWindow: window,
}),
);
${generatedCode || "export const Widget = () => <div>No code generated yet.</div>"}
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")).render(<Widget />);
</script>
</body>
</html>
`;
return <iframe srcDoc={srcDoc} />;
};Contributing
Contributions are welcome! Please open an issue or a pull request if you find any improvements, bug fixes, or have suggestions.
When contributing, please follow the project's coding style which prefers the use of arrow functions in TypeScript.
