# How to Add a New RPC Endpoint to the ACN Daemon in Magnitude

> Learn to add a new RPC endpoint to the ACN daemon in magnitudedev/magnitude. Discover how to declare contracts, register handlers with Effect-TS, and expose via SDK.

- Repository: [Magnitude/magnitude](https://github.com/magnitudedev/magnitude)
- Tags: how-to-guide
- Published: 2026-09-08

---

**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`](https://github.com/magnitudedev/magnitude/blob/main/sessions.ts) or [`examples.ts`](https://github.com/magnitudedev/magnitude/blob/main/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:

```typescript
// 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`](https://github.com/magnitudedev/magnitude/blob/main/packages/acn-protocol/src/boundary/index.ts) and passing it to `RpcGroup.make`:

```typescript
// 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`](https://github.com/magnitudedev/magnitude/blob/main/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:

```typescript
// 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`](https://github.com/magnitudedev/magnitude/blob/main/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`:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/acn/src/example-service.test.ts) to test the handler in isolation using Effect's test suite.
- **Integration tests**: Add [`packages/sdk/src/jit-rpc/example.test.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/sdk/src/jit-rpc/example.test.ts) to 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.make` in `packages/acn-protocol/src/boundary/` to declare the contract with strict schemas.
- **Group registration**: Add the RPC to `AcnRpcGroup` in [`packages/acn-protocol/src/boundary/index.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/acn-protocol/src/boundary/index.ts) to expose it to the daemon.
- **Handler implementation**: Write the logic in `packages/acn/src/` using Effect-TS primitives, then wire it via `Effect.provideService` in [`server.ts`](https://github.com/magnitudedev/magnitude/blob/main/server.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.