How to Add a New RPC Endpoint to the ACN Daemon in Magnitude
Adding a new RPC endpoint to the ACN daemon requires declaring the contract in the protocol layer, registering it in the AcnRpcGroup, implementing the handler using Effect-TS, and exposing it through the SDK.
Extending the ACN (Agent Communication Network) daemon with new capabilities follows a strict architectural pattern across the magnitudedev/magnitude monorepo. The process leverages Effect-TS for type-safe error handling and dependency injection, ensuring that remote procedure calls are validated at the protocol boundary before reaching the daemon's core logic.
Step 1: Declare the RPC in the Protocol Layer
All RPC contracts originate in the acn-protocol package. Create a new module in packages/acn-protocol/src/boundary/<domain>.ts (for example, sessions.ts or examples.ts) and define the RPC using Rpc.make from @effect/rpc.
The declaration specifies the RPC name, request payload schema, and response schema using Effect-TS schemas:
// packages/acn-protocol/src/boundary/example.ts
import { Rpc, Schema } from "@effect/rpc"
export const GetExample = Rpc.make("GetExample", {
payload: Schema.struct({ id: Schema.String }),
success: Schema.struct({ name: Schema.String, value: Schema.Number }),
})
This creates a type-safe contract that both the server and client will reference.
Step 2: Register the RPC in the AcnRpcGroup
The ACN daemon exposes RPCs through a centralized AcnRpcGroup. Register your new RPC by importing it into packages/acn-protocol/src/boundary/index.ts and passing it to RpcGroup.make:
// packages/acn-protocol/src/boundary/index.ts
import { RpcGroup } from "@effect/rpc"
import { GetExample } from "./example"
// ... other imports
export const AcnRpcGroup = RpcGroup.make({
// ...existing RPCs,
GetExample,
})
This aggregation step is critical because server.ts imports the group as a single protocol unit.
Step 3: Implement the Handler in the ACN Daemon
Implement the business logic in a service module within packages/acn/src/. The handler must return an Effect that fulfills the contract defined in Step 1:
// packages/acn/src/example-service.ts
import { GetExample } from "@magnitudedev/acn-protocol"
import { Effect } from "effect"
export const ExampleService = Effect.succeed({
getExample: ({ id }) =>
Effect.succeed({ name: `Item-${id}`, value: 42 })
})
Next, wire the implementation into the HTTP router in packages/acn/src/server.ts. The server uses RpcServer.makeProtocolHttpRouter to create an HTTP router at /rpc, then provides the service implementation using Effect.provideService:
// packages/acn/src/server.ts
import { RpcServer } from "@effect/rpc"
import { AcnRpcGroup } from "@magnitudedev/acn-protocol"
import { ExampleService } from "./example-service"
const rawProtocol = yield* RpcServer.makeProtocolHttpRouter({ path: "/rpc" })
const protocol = yield* RpcServer.make(AcnRpcGroup).pipe(
Effect.provideService(RpcServer.Protocol, rawProtocol),
Effect.provideService(GetExample, ExampleService) // Register handler
)
The RpcServer.make function creates the protocol handler, while Effect.provideService injects the concrete implementation into the Effect context.
Step 4: Expose the RPC Through the SDK
The SDK layer in packages/sdk/src/jit-rpc/ provides a typed client interface. Create a function that calls the RPC using the generated client:
// packages/sdk/src/jit-rpc/example.ts
import { GetExample } from "@magnitudedev/acn-protocol"
export const getExample = (id: string) =>
rpcClient.call(GetExample, { id })
The SDK handles RPC serialization and endpoint resolution automatically, transforming the Effect-based protocol into a Promise-based API for consumers.
Step 5: Add a First-Party Operation (Optional)
For UI or CLI consumption, wrap the SDK call in a higher-level operation within packages/client-common/src/operations/. This layer adds caching, synchronization, and UI-specific error handling:
// packages/client-common/src/operations/example.ts
import { getExample } from "@magnitudedev/sdk"
import { Effect } from "effect"
export const fetchExample = (id: string) =>
Effect.flatMap(getExample(id), (result) => Effect.succeed(result))
Operations in client-common abstract the raw RPC into domain-specific queries or mutations that the frontend can consume directly.
Step 6: Update Tests and Documentation
Maintain architectural integrity by adding comprehensive tests:
- Unit tests: Create
packages/acn/src/example-service.test.tsto test the handler in isolation using Effect's test suite. - Integration tests: Add
packages/sdk/src/jit-rpc/example.test.tsto verify the end-to-end serialization and transport layer. - Documentation: Update JSDoc comments in the protocol declaration and ensure design documents in
design/reflect the new capability.
Summary
- Protocol definition: Use
Rpc.makeinpackages/acn-protocol/src/boundary/to declare the contract with strict schemas. - Group registration: Add the RPC to
AcnRpcGroupinpackages/acn-protocol/src/boundary/index.tsto expose it to the daemon. - Handler implementation: Write the logic in
packages/acn/src/using Effect-TS primitives, then wire it viaEffect.provideServiceinserver.ts. - SDK exposure: Create a wrapper in
packages/sdk/src/jit-rpc/that calls the RPC through the generated client. - Optional operations: For UI consumption, add a layer in
packages/client-common/src/operations/to handle caching and synchronization. - Testing: Verify functionality with unit tests for handlers and integration tests for the SDK layer.
Frequently Asked Questions
What is the ACN daemon in Magnitude?
The ACN (Agent Communication Network) daemon is the centralized server process in magnitudedev/magnitude that manages agent sessions, orchestrates test execution, and exposes administrative RPC endpoints. It listens on an HTTP interface at the /rpc path and uses Effect-TS to handle concurrent, type-safe communication between the CLI, web UI, and background agents.
Why does Magnitude use Effect-TS for RPC handling?
Effect-TS provides structured concurrency, composable error handling, and type-safe dependency injection that makes the ACN daemon resilient to failures. The Rpc.make and RpcGroup.make APIs generate runtime validators from static TypeScript types, ensuring that requests and responses match the protocol schema before reaching business logic, eliminating an entire class of runtime serialization errors.
How do I handle errors in RPC handlers?
Return Effect.fail with a typed error from your service implementation. The protocol layer will serialize the error according to the schema defined in Rpc.make. In the SDK layer, these errors propagate as rejected promises or failed Effects that you can handle using Effect.catchAll or standard try/catch when consuming the client.
What is the difference between the SDK and client-common layers?
The SDK layer (packages/sdk/) provides thin, generated wrappers around raw RPC calls that handle transport and serialization. The client-common layer (packages/client-common/) sits above the SDK and adds application-specific concerns like React Query-style caching, optimistic updates, and error normalization for the web interface or CLI. Not all RPCs need a client-common operation, but all must pass through the SDK.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →