사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
CLI 레퍼런스▾
AkanJS 레퍼런스▾
akanjs/signal
akanjs/signal은 서비스를 둘러싼 API 경계를 선언합니다. 무엇을, 누가, 어떤 전송 방식으로 호출할 수 있는지 정합니다. *.signal.ts 파일과, 가드·미들웨어를 두는 srvkit/ 파일에서 import합니다.주요 export
export설명
endpoint
모듈이 공개하는 호출을 선언합니다. 조회, 변경, WebSocket 엔드포인트가 여기에 들어갑니다.
internal
서버가 스스로 시작하는 작업을 선언합니다. 스케줄, 라이프사이클 훅, 큐 작업, resolve 필드가 여기에 들어갑니다.
slice
클라이언트 store가 불러올 목록 쿼리를 선언하고, 자동 생성되는 CRUD 엔드포인트의 가드를 정합니다.
PublicNoneguard
가드입니다. 핸들러보다 먼저 실행되어 호출을 계속할지 정합니다.
ReqResIpWs
내부 인자(internal argument)입니다. 요청 객체처럼 호출자가 아니라 서버가 채워 주는 핸들러 인자입니다.
middlewareLoggingTimeout
미들웨어는 모든 엔드포인트 호출을 감쌉니다. 기본 제공 두 개는 처음부터 등록되어 있습니다.
McpProgress
오래 걸리는 MCP 툴 호출 안에서 진행률을 보고합니다.
SignalRegistryserverSignal
등록된 signal을 찾고, 서비스에서 발행하거나 큐에 작업을 넣습니다.
SignalContext
가드와 미들웨어가 받는 호출 단위 context입니다. 전송 방식, 인자, 호출자를 담습니다.
엔드포인트 네 종류
endpoint 빌더는 네 종류를 제공하며, 종류마다 전송 방식이 정해져 있습니다:종류
HTTP
WebSocket
요청과 응답
query
✓
GET으로 읽습니다. cache를 선언할 수 있는 유일한 종류입니다.mutation
✓
POST로 씁니다. method 옵션으로 PATCH, PUT, DELETE로 바꿀 수 있습니다.실시간
pubsub
✓
클라이언트가 room을 구독하고, 서버가 그 room에 발행합니다.
message
✓
클라이언트가 메시지 하나를 보내고 응답 하나를 받습니다.
✓이 방식으로 전송해당 없음
MCP 프롬프트는 엔드포인트 종류가 아닙니다. 페이지에서
page().prompt(name, description)로 선언합니다.Public / None / guard
가드는 핸들러보다 먼저 실행되어 호출을 통과시킬지 정합니다. 직접 만든 엔드포인트는 모두 자기
guards 배열을 선언하고, 그 안의 가드를 전부 통과해야 합니다.export설명
Public
항상 통과합니다. slice의
get: 같은 읽기에 쓰고, mutation의 유일한 가드로는 쓰지 않습니다.None
호출을 항상 거절합니다.
guard(name)
static name이 채워져 있고 scope가 "account"로 미리 정해진 기반 클래스입니다.Guard
인터페이스입니다.
canPass(context)가 boolean이나 boolean의 Promise를 반환합니다.GuardScope
"account" 또는 "resource"입니다. 가드가 판정하는 데 무엇이 필요한지 나타냅니다.account와 resource
모든 가드에는
static scope가 있으며, 판정에 호출 인자가 필요한지를 나타냅니다:scope
인자 읽음
MCP 목록에서 평가
GuardScope
"account"
✓
호출자만 읽습니다.
context.get("account")를 씁니다."resource"
✓
context.getArg(name)로 호출 인자를 읽으므로, 호출할 때에만 판정합니다.✓예아니요
- 역할 검사는
"account"입니다.libs/shared의Every,Admin,Person은"account"이고,Owner,SelfOrAdmin, 그리고Can<Verb><Model>형태의 가드는"resource"입니다. implements Guard로 만들면 직접 선언합니다.guard(name)은"account"가 기본값이므로, 이를 상속해 인자를 읽는 가드는"resource"로 덮어씁니다.
가드 작성하기
가드는
srvkit/guards.ts에 가드마다 클래스 하나로 둡니다:apps/koyo/srvkit/guards.ts
그다음 엔드포인트마다 가드를 적습니다.
pubsub room은 slice 가드가 덮지 않으므로 직접 선언합니다:apps/koyo/lib/notice/notice.signal.ts
- 전송 방식과 상관없이 같은 가드입니다. 가드는 HTTP 호출과 WebSocket 호출에서 똑같이 실행됩니다. 전송 방식으로 분기하지 말고
context.get("account")로 호출자를 읽으세요. - 선언한 순서대로, 처음 거절에서 멈춥니다. 가드는 적은 순서대로 실행되고, 처음 거절한 가드에서 호출이 멈춥니다.
- slice 가드는 CRUD까지만 덮습니다. slice의
guards는 자동 생성된 query와 mutation 엔드포인트에만 적용됩니다.pubsub와message는 각자 선언합니다. static name을 지우지 마세요. fetch가 가드 이름을 직렬화하고, API 탐색기가 그 이름으로 필터링합니다.


scope를 잘못 표기하면 MCP 목록이 틀어집니다. 노출 여부는 가드를 따르므로, 호출자만 읽는 가드를
"resource"로 표기하면 쓸 수 없는 호출자에게도 엔드포인트가 보이고, 인자를 읽는 가드를 "account"로 표기하면 모두에게서 숨겨집니다.McpProgress
McpProgress는 오래 걸리는 MCP 툴 호출이 어디까지 진행됐는지 보고해서, 에이전트 쪽 클라이언트가 보여 줄 수 있게 합니다. 실제 작업이 일어나는 곳에서 바로 호출하면 되고, 인자로 넘겨줄 것은 없습니다.McpProgress.report(progress, option?)(progress: number, option?: McpProgressOption) => void
지금 실행 중인 호출의 진행률 알림을 하나 보냅니다.
option.totalnumber
선택입니다. 클라이언트가 표시할 분모이며, 전체 작업량을 모르면 생략합니다.
option.messagestring
선택입니다. 현재 단계를 설명하는 짧은 한 줄이며, 사용자가 읽으므로 문장으로 씁니다.
McpProgress.streamingboolean
클라이언트가 스트리밍 중일 때만
true입니다. 만들기 비싼 메시지를 건너뛸 때 씁니다.행을 가져오는 서비스가 한 행마다 진행률을 보고합니다:
apps/koyo/lib/task/task.service.ts
- 스트림이 아니면 아무것도 하지 않습니다. 일반 HTTP, WebSocket, 테스트에서는
report가 무시되므로 같은 코드가 그대로 동작합니다. - 어느 깊이에서든 부를 수 있습니다.
AsyncLocalStorage를 쓰기 때문에 서비스, 어댑터, 몇 단계 아래의 반복문에서도 채널 인자 없이 보고합니다. - 클라이언트가 요청해야 스트리밍합니다. 요청에
Accept: text/event-stream과_meta.progressToken이 모두 있어야 합니다. - 첫 보고가 도착해야 응답 방식이 바뀝니다. 그때 서버가 SSE로 응답하고, 한 번도 보고하지 않은 호출은 일반 응답을 받습니다.
Req / Res / Ip / Ws
내부 인자(internal argument)는 호출자가 아니라 서버가 채워 주는 핸들러 인자입니다.
.with(X)로 선언하면 핸들러는 선언된 인자 다음 순서로 이 값을 받습니다.인자
HTTP
query · mutation
WebSocket
pubsub · message
요청
Req
✓
현재 Bun 요청(
Bun.BunRequest)입니다.Res
✓
응답을 만드는
Response 클래스입니다. res.json(value)처럼 씁니다.호출자
Ip
✓
✓
가장 가까운 프록시가 기록한 호출자 IP입니다. 알 수 없으면
null입니다.연결
Ws
✓
ws, socketId, subscribe, 그리고 정리용 on / off 훅입니다.✓사용 가능사용 불가
라이브러리도 자기 인자를 제공합니다. 예를 들어
@libs/shared/srvkit에는 Self, Me, Account가 있습니다. 호출자는 이 인자로 받고, 클라이언트가 보낸 id는 믿지 마세요.원본 요청 본문과 호출자 IP를 읽는 mutation, 그리고 소켓이 닫힐 때 정리하는 메시지 핸들러입니다:
apps/koyo/lib/_wallpad/wallpad.signal.ts
- nullable이 아니면 필수입니다. 필수 내부 인자가
null이면 호출은 401로 거절됩니다.Ip처럼null도 정상 값이면{ nullable: true }를 넘기세요. Response를 반환하면 그대로 전송됩니다. 직렬화를 건너뛰므로, 반환 타입이Any인 엔드포인트가 파일을 그대로 흘려보낼 수 있습니다.- 정리 함수는 등록한 호출의 범위를 따릅니다.
on("disconnect" | "unsubscribe", fn)은pubsub에서는 room에,message에서는 소켓에 묶입니다. 어느 쪽이든 실행돼야 하면 둘 다 등록하세요. socketId는 호출자가 아니라 연결을 가리킵니다. 사용자별 상태는 계정 기준으로 저장하고, id를 직접 만들지 마세요.


호출자 IP를 소켓이나 요청에서 직접 읽지 마세요. gateway 뒤에서는 모든 호출자의 peer가 gateway 자신(
127.0.0.1)입니다. .with(Ip)를 쓰세요.middleware / Middleware
미들웨어는 모든 엔드포인트 호출을 핸들러 앞뒤로 감쌉니다. 기본으로 두 개가 등록되어 있고, 직접 만들 때는
middleware(refName)을 씁니다.| 미들웨어 | 동작 조건 |
|---|---|
| ↳ 하는 일 | |
Logging | 항상 동작합니다. |
| 호출 앞뒤로 debug 로그를 남기고, 실패하면 error 로그를 남깁니다. | |
Timeout | 엔드포인트가 timeout(ms)을 선언했을 때. |
시간이 다 되면 base.error.gatewayTimeout(504)으로 거절합니다. | |
엔드포인트의
{ cache: <ms> }는 미들웨어가 아닙니다. 저장된 응답은 호출 안에서 가드 다음에 찾으므로, 캐시 적중도 가드가 통과시킨 호출자에게만 돌아갑니다.실행 순서
바깥쪽부터 차례로 실행됩니다:
- 기본 미들웨어 두 개:
Logging→Timeout. lib/option.ts가applyMiddleware(...)로 추가한 미들웨어. 예를 들어libs/shared의AccountMiddleware가 있습니다.- 엔드포인트 옵션의
middlewares. - 가드, 내부 인자, 엔드포인트가
cache를 선언했다면 캐시 조회, 그리고 핸들러.
직접 만들기
느린 호출을 경고하는 미들웨어입니다:
apps/koyo/srvkit/slowCallMiddleware.ts
등록하는 곳은 두 군데입니다:
등록 위치설명
lib/option.ts
서버가 실행하는 모든 엔드포인트에 적용되며, 기본 두 개 다음에 실행됩니다.
middlewares
엔드포인트 옵션입니다. 그 엔드포인트에만 적용되며, 전역 미들웨어보다 안쪽에서 실행됩니다.
use(env)는 한 번만 실행됩니다. 반환한 핸들러를 모든 호출이 재사용하므로, 준비 작업은use에서, 호출마다 할 일은 핸들러 안에서 합니다.next()를 건너뛰면 가드도 건너뜁니다. 가드는next()안에서 실행되므로, 가드가 걸린 엔드포인트를 대신해 미들웨어가 직접 응답하면 안 됩니다. 저장해 둔 응답이 필요하면 엔드포인트에{ cache: <ms> }를 선언하세요.refName이 식별자입니다. 이미 쓰인refName으로 등록하면 앞의 미들웨어를 대체합니다.


timeout은 작업을 취소하지 않습니다. 결과를 받을 곳이 없어도 핸들러는 끝까지 실행되므로 쓰기 작업은 그대로 일어납니다. 마감 시간은 호출자에게 답할 뿐, 호출을 되돌리지 않습니다.
SignalRegistry
SignalRegistry는 런타임에 refName으로 모듈의 등록된 signal을 찾습니다. 등록되지 않은 refName이면 undefined를 반환합니다.메서드설명
getDatabase(refName)
데이터베이스 모듈의
internal, endpoint, slice, server, serializedSignal을 돌려줍니다.getService(refName)
서비스 모듈의
internal, endpoint, server, serializedSignal을 돌려줍니다.refName으로 찾습니다:
apps/koyo/srvkit/signalLookup.ts
서비스에서 발행하기
모듈마다 server signal도 있으며, 서비스가
signal<sig.X>()로 주입받습니다. 엔드포인트와 internal이 메서드가 됩니다:메서드설명
<pubsubKey>(...roomArgs, data)
pubsub 엔드포인트마다 하나씩 생깁니다. 인자가 가리키는 room에 data를 발행합니다.<processKey>(...args, jobOptions?)
process internal마다 하나씩 생깁니다. 큐에 작업을 넣고 AkanJob을 반환합니다.공지 서비스가 공지를 저장한 뒤, 가드 예제의
noticeAdded room에 발행합니다:apps/koyo/lib/notice/notice.service.ts
- 먼저 저장하고 그다음 알립니다. 그래야 저장에 실패한 데이터가 구독자에게 가지 않습니다.
- room의 반환 모델이 데이터 모양을 정합니다.
data는 일반 응답처럼pubsub엔드포인트의 반환 모델로 직렬화됩니다. - 필드 이름이 signal을 고릅니다.
noticeSignal은notice모듈의 server signal로 연결되며,Signal접미사는 필수입니다.