@isdk/tool-rpc is a powerful TypeScript library that extends the modular capabilities of @isdk/tool-func to the network. It allows you to easily expose ToolFunc instances as a remote service and call them seamlessly from a client, just like local functions. It's perfect for building distributed AI agents, microservice architectures, or any scenario that requires tool invocation across different processes or machines.
This project is built on @isdk/tool-func. Before proceeding, please ensure you are familiar with its core concepts.
- Layered Abstractions: Provides a set of layered tool classes (
ServerTools,RpcMethodsServerTool,ResServerTools) that allow you to choose the right level of abstraction for your needs. - π RESTful Interface: Quickly create REST-style APIs using
ResServerTools, which automatically handles standard operations likeget,list,post,put, anddelete. - π§ RPC Method Grouping: Use
RpcMethodsServerToolto bundle multiple related functions (methods) under a single tool, invoked via anactparameter. - π Automatic Client-Side Proxies: The client (
ClientToolsand its subclasses) can automatically load tool definitions from the server and dynamically generate type-safe proxy functions, making remote calls as simple as local ones. - π Built-in HTTP Transport: Comes with
HttpServerToolTransportbased on Node.js'shttpmodule andHttpClientToolTransportbased onfetch, ready to use out of the box. - π Streaming Support: Both server and client support
ReadableStream, enabling easy implementation of streaming data responses.
npm install @isdk/tool-rpc @isdk/tool-funcThe following example demonstrates how to create a RESTful tool, expose it on a server, and call it from a client.
Create a class that extends ResServerTools. Implement standard methods like get and list, as well as any custom business logic in methods prefixed with $.
// ./tools/UserTool.ts
import { ResServerTools, ResServerFuncParams } from '@isdk/tool-rpc';
import { NotFoundError } from '@isdk/common-error';
// In-memory database for demonstration
const users: Record<string, any> = {
'1': { id: '1', name: 'Alice' },
'2': { id: '2', name: 'Bob' },
};
export class UserTool extends ResServerTools {
// Standard RESTful method: GET /api/users/:id
get({ id }: ResServerFuncParams) {
const user = users[id as string];
if (!user) throw new NotFoundError(id as string, 'user');
return user;
}
// Standard RESTful method: GET /api/users
list() {
return Object.values(users);
}
// Custom RPC-style method
$promote({ id }: ResServerFuncParams) {
const user = users[id as string];
if (!user) throw new NotFoundError(id as string, 'user');
return { ...user, role: 'admin' };
}
}In your server entry file, instantiate your tools, then set up and start the HttpServerToolTransport.
// ./server.ts
import { HttpServerToolTransport, ResServerTools } from '@isdk/tool-rpc';
import { UserTool } from './tools/UserTool';
async function startServer() {
// 1. Instantiate and register your tool.
// The name 'users' will be used as part of the URL (e.g., /api/users)
new UserTool('users').register();
// 2. Initialize the server transport.
const serverTransport = new HttpServerToolTransport();
// 3. Mount the tool's base class. The transport will find all registered
// instances of ResServerTools.
// This creates API endpoints under the '/api' prefix.
serverTransport.mount(ResServerTools, '/api');
// 4. Start the server.
const port = 3000;
await serverTransport.start({ port });
console.log(`β
Tool server started, listening on http://localhost:${port}`);
}
startServer();In your client-side code, initialize the HttpClientToolTransport, which will automatically load tool definitions from the server and create proxies.
// ./client.ts
import { HttpClientToolTransport, ResClientTools } from '@isdk/tool-rpc';
// Define a type for full type-safety, including the custom method
type UserClientTool = ResClientTools & {
promote(params: { id: string }): Promise<{ id: string; name: string; role: string }>;
};
async function main() {
const apiRoot = 'http://localhost:3000/api';
// 1. Initialize the client transport.
const clientTransport = new HttpClientToolTransport(apiRoot);
// 2. Mount the client tools. This action will:
// a. Set the transport for ResClientTools.
// b. Load API definitions from the server and create proxy tools.
await clientTransport.mount(ResClientTools);
// 3. Get the dynamically created proxy for the remote tool.
const userTool = ResClientTools.get('users') as UserClientTool;
if (!userTool) {
throw new Error('Remote tool "users" not found!');
}
// 4. Call the remote APIs as if they were local methods!
// Calls GET /api/users/1
const user = await userTool.get!({ id: '1' });
console.log('Fetched User:', user); // { id: '1', name: 'Alice' }
// Calls GET /api/users
const allUsers = await userTool.list!();
console.log('All Users:', allUsers); // [{...}, {...}]
// Calls the custom RPC method $promote
// The client proxy automatically handles the `act` parameter.
const admin = await userTool.promote({ id: '2' });
console.log('Promoted User:', admin); // { id: '2', name: 'Bob', role: 'admin' }
}
main();The core design of @isdk/tool-rpc is a layered architecture that cleanly separates network communication from business logic. You can choose the right level of abstraction based on the complexity of your needs.
graph TD
subgraph Client-Side
A[Client Application] --> B{ResClientTools};
B --> C{RpcMethodsClientTool};
C --> D{ClientTools};
end
subgraph Transport Layer
D --> |Uses| E[IClientToolTransport];
E -- HTTP Request --> F[IServerToolTransport];
end
subgraph Server-Side
F -->|Calls| G{ServerTools};
H{ResServerTools} --> I{RpcMethodsServerTool};
I --> G;
end
A -- Calls method on instance --> B;
B -- Transparently calls --> E;
F -- Receives request and finds --> H;
H -- Executes business logic --> H;
H -- Returns result through --> F;
F -- Sends HTTP Response --> E;
E -- Returns result to --> B;
B -- Returns result to --> A;
You might notice that the class names ServerTools, ClientTools, etc., are plural. This is an intentional design choice that reflects their dual role:
-
The Static Class as a Registry (The Plural 'Tools'): The static part of
ServerTools(e.g.,ServerTools.register(),ServerTools.items) acts as a global registry and manager for all remote tools. It holds the collection of tools and provides static methods to manage them. This is where the "Tools" (plural) in the name comes from. -
An Instance as a Single Tool (The Singular 'Tool'): When you create an instance (e.g.,
new ServerTools({ name: 'myTool', ... })), you are defining a single, concrete tool. The instance encapsulates the logic, metadata, and configuration for one individual function.
In short: The class is the collection of tools; an instance is a single tool. This design provides a clean separation between the management of tools (static) and the definition of a tool (instance).
This is the most fundamental layer, representing a single, remotely callable function.
-
ServerTools:-
Concept: An individual, remotely executable function.
-
Use Case: This is the simplest approach when you just need to expose a few scattered functions that don't have a strong relationship with each other.
-
Advanced Usage: Inside the
func, you can access the raw HTTP request and response objects viaparams._reqandparams._resfor lower-level control. -
Example:
// server.ts new ServerTools({ name: 'ping', isApi: true, // Mark as discoverable func: () => 'pong', }).register();
-
-
ClientTools:- Concept: The client-side proxy for a
ServerToolsinstance on the server. - How it Works: After
client.init(), it creates aClientToolsinstance namedping. When you callToolFunc.run('ping'), it sends the request over the network to the server.
- Concept: The client-side proxy for a
This layer organizes multiple related functions into a collection that resembles an "object" or "service".
-
RpcMethodsServerTool:-
Concept: A "service" object containing multiple callable methods. It acts as a dispatcher.
-
Use Case: Use this class to better organize your code when you have a group of cohesive operations (e.g., a
UserServicewithcreateUser,updateUser,getUser). -
How it Works: Methods defined in the class with a
$prefix (e.g.,$createUser) are automatically registered as RPC methods. The client specifies which method to call by passingact: '$createUser'in the request. -
Example:
// server.ts class UserService extends RpcMethodsServerTool { $createUser({ name }) { // ... logic to create a user return { id: 'user-1', name }; } $getUser({ id }) { // ... logic to get a user return { id, name: 'Test User' }; } } new UserService('userService').register();
-
-
RpcMethodsClientTool:-
Concept: The client-side proxy for the remote service object.
-
How it Works: On initialization, it detects the
$createUserand$getUsermethods on the server and dynamically creates correspondingcreateUser()andgetUser()methods on the client instance. This makes the invocation look like a local object method call, completely hiding the underlyingactparameter and network communication. -
Example:
// client.ts const userService = RpcMethodsClientTool.get('userService'); const newUser = await userService.createUser({ name: 'Alice' });
-
This is the highest-level abstraction, providing a resource-centric, RESTful-style API on top of RPC.
-
ResServerTools:- Concept: Represents a RESTful resource with built-in mapping for standard HTTP verbs (GET, POST, PUT, DELETE).
- Use Case: Ideal for scenarios requiring standard CRUD (Create, Read, Update, Delete) operations, such as managing
usersorproductsresources. - How it Works: It extends
RpcMethodsServerTooland pre-defines special methods likeget,list,post,put,delete. The HTTP transport layer intelligently calls the appropriate method based on the request'smethod(GET/POST) and whether anidis present in the URL.GET /users/:id->get({ id })GET /users->list()POST /users->post({ val })
- Advanced Usage: Since it extends
RpcMethodsServerTool, you can still define custom$methods in aResServerToolssubclass, allowing you to combine RESTful patterns with specific RPC calls (as shown in the Quick Start example on this page).
-
ResClientTools:-
Concept: The client-side proxy for a remote RESTful resource.
-
How it Works: It provides a set of convenient methods like
.get(),.list(),.post(), etc., which automatically construct and send requests that conform to REST semantics. -
Example:
// client.ts const userRes = ResClientTools.get('users'); const user = await userRes.get({ id: '1' }); // Sends GET /api/users/1 const allUsers = await userRes.list(); // Sends GET /api/users
-
The transport layer is a core pillar of @isdk/tool-rpc, acting as the communication bridge between server tools and client tools to enable true Remote Procedure Calls (RPC).
The core design philosophy of the transport layer is the separation of concerns. It completely decouples the business logic of your tools (what you define in a Tool) from the implementation details of network communication (like protocol, routing, and serialization). This keeps your tool code pure and portable, without needing to know whether it's communicating over HTTP, WebSockets, or any other protocol. You define what your tool does, and the transport layer handles the rest.
The architecture is built around a few key interfaces:
IToolTransport: The common base interface for all transports, defining basic operations likemount.IServerToolTransport: The interface that a server-side transport must implement. Its core responsibilities are:- Exposing a Discovery Endpoint: Create a route, typically
GET(e.g.,/api), which, when accessed by a client, returns the JSON definitions of all registered and available tools. This is achieved via theaddDiscoveryHandlermethod. - Handling RPC Calls: Create a generic RPC route (e.g.,
/api/:toolId) that receives requests, finds the corresponding tool bytoolId, executes it, and returns the result. This is handled by theaddRpcHandlermethod. - Managing the server lifecycle (
start,stop).
- Exposing a Discovery Endpoint: Create a route, typically
IClientToolTransport: The interface that a client-side transport must implement. Its core responsibilities are:- Loading API Definitions: Call the
loadApis()method, which accesses the server's discovery endpoint to get the definitions of all tools. - Executing Remote Calls: Implement the
fetch()method, which is responsible for serializing the client's tool call (function name and parameters), sending it to the server's RPC endpoint, and processing the response.
- Loading API Definitions: Call the
The library provides a plug-and-play, HTTP-based transport implementation that requires no extra configuration:
-
HttpServerToolTransport: A server-side transport that uses Node.js's built-inhttpmodule to create a lightweight server. When you callserverTransport.mount(ServerTools, '/api'), it automatically:- Creates a
GET /apiroute for service discovery. - Creates a
POST /api/:toolIdroute (and supports other methods) to handle RPC calls for all tools. It intelligently parses tool parameters from the request body or URL parameters.
- Creates a
-
HttpClientToolTransport: A client-side transport that uses the cross-platformfetchAPI to send requests to the server. When you callclient.init()(which usesloadApisinternally), it requestsGET /api. When you run a tool, it sends a JSON request with parameters toPOST /api/toolName.
@isdk/tool-rpc is designed to be fully extensible. You can create your own transport layer to support different protocols (like WebSockets, gRPC) or to integrate with existing web frameworks (like Express, Koa, or Fastify) by implementing the interfaces described above.
Idea for a Fastify Integration:
- Create a
FastifyServerTransportclass that implementsIServerToolTransport. - In the
mountmethod, instead of creating a newhttpserver, you would accept an existingFastifyInstanceas an option. - Use
fastify.get(apiPrefix, ...)to register the discovery route, with a handler that callsServerTools.toJSON(). - Use
fastify.all(apiPrefix + '/:toolId', ...)to register the RPC route. The handler would extract parameters fromrequest.bodyorrequest.query, call the appropriate tool, and usereplyto send the response. - The
startandstopmethods could delegate to the Fastify instance'slistenandclosemethods.
This pluggable architecture gives @isdk/tool-rpc immense flexibility, allowing it to easily adapt to various project requirements and tech stacks.
This section covers some of the more powerful features of @isdk/tool-rpc for building flexible and efficient applications.
In some scenarios, you might want to offload computation from the server or allow the client to execute a function locally. The allowExportFunc option enables this by serializing the function's body and sending it to the client during the discovery phase. When set to true, the client-side ClientTools will automatically use this downloaded function instead of making a network request.
Server-Side Configuration:
// server.ts
new ServerTools({
name: 'local-uuid',
isApi: true,
allowExportFunc: true, // Allow this function to be downloaded
func: () => {
// This logic will be executed on the client
console.log('Generating UUID on the client...');
return Math.random().toString(36).substring(2, 15);
},
}).register();Client-Side Usage:
// client.ts
const uuidTool = ClientTools.get('local-uuid');
// This call executes the downloaded function body. No network request is made.
const uuid = await uuidTool.run();
console.log('Generated UUID:', uuid);When using ResServerTools, the framework intelligently routes incoming HTTP requests. It inspects the HTTP method and the presence of an id parameter to determine which function to call. This logic, handled by getMethodFromParams within the tool, provides the following mappings out-of-the-box:
GET /api/my-resourceβlist()GET /api/my-resource/123βget({ id: '123' })POST /api/my-resourceβpost({ val: ... })
This allows you to write clean, resource-oriented code without worrying about manual routing.
Since ResServerTools extends RpcMethodsServerTool, you can combine both API styles in a single tool. You can implement standard RESTful methods (get, list) while also adding custom RPC-style methods (e.g., $archive, $publish) for specific business operations that don't fit the CRUD model.
The params schema you define on a tool is used for more than just documentation. The server automatically uses it to cast incoming parameters to their specified types. For example, if you define id: { type: 'number' }, any id received from a URL path (which is a string) will be converted to a Number before your function is called.
For convenience, when you define a method with a $ prefix on the server (e.g., $promoteUser), the client-side proxy automatically creates a cleaner alias without the prefix. This allows you to call myTool.promoteUser(...) on the client, making the code more readable and idiomatic.
We welcome contributions of all kinds! Please read the CONTRIBUTING.md file for guidelines on how to get started.
This project is licensed under the MIT License. See the LICENSE-MIT file for more details.