사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.











POST /mcp에서 AI 에이전트에게 그대로 제공됩니다. 별도의 API도, 시그널 파일에 더 적을 것도 없이 같은 엔드포인트가 같은 가드, 미들웨어, 서비스를 거칩니다. 내 페이지 안의 채팅은 다른 표면인 인페이지 에이전트입니다.akan:// URI로 가리키는 조회입니다. 클라이언트가 컨텍스트로 붙일 수 있습니다.akan:// 리소스 URI도 받습니다.| 거부 조건 |
|---|
| ↳ 이유와 대처 |
mcp: false를 선언했다 |
| 일부러 선반에서 뺀 것입니다. 가드와 HTTP는 그대로입니다. |
Person처럼 static agents = false인 가드가 붙어 있다 |
| 사람만 할 수 있는 행위라 어떤 모델에게도 내밀지 않습니다. |
guards를 적지 않았거나 빈 배열이다 |
누가 호출할지 정한 적이 없습니다. 익명 호출이 의도라면 guards: [Public]을 적으세요. |
생성된 light<Model> 조회다 |
<model>과 같은 도큐먼트를 작은 형태로 읽을 뿐이라, 에이전트는 대신 <model>을 호출합니다. |
pubsub이나 message다 |
| 웹소켓으로 동작하고, 인자가 MCP 요청에는 없는 소켓을 읽습니다. |
읽기 전용 배포인데 query가 아니다 |
readOnly 밸브는 가드가 무엇을 허용하든 모든 mutation을 뺍니다. |
Any, Upload, Binary를 반환한다 |
| 무엇이 돌아오는지 모델에게 설명할 수 없고, 원시 바이트는 컨텍스트 창만 채웁니다. |
| 파일 업로드를 받는다 |
| 파일 업로드는 MCP로 표현할 방법이 없습니다. |
가드가 Public 하나뿐인 mutation이다 |
쓰기에 [Public]만 붙인 것은 가드가 없다는 말을 풀어 쓴 것입니다. 실제 가드를 붙이세요. |
필수 인자의 타입이 Any다 |
Any는 스키마에서 빠지므로, 대신 이름 있는 filter 슬라이스를 노출하세요. |


Unknown tool로 답하고, 가드의 거절은 가드 이름 없이 항상 You are not permitted to perform this action.입니다. 두 메시지를 더 친절하게 만들지 마세요. 그 차이가 비공개 표면을 하나하나 알아내는 단서가 됩니다./mcp는 기본으로 마운트되므로 새 앱도 이미 제공하고 있습니다. 설정은 main.ts가 아니라 lib/option.ts에서 setMcp()로 바꿉니다:option.ts를 마운트 순서대로 읽고 앱의 것을 마지막에 읽으므로, 최종 결정은 앱이 합니다.AKAN_MCP_* env보다 우선하지만, undefined를 쓴다고 env 값이 지워지지는 않습니다. AKAN_MCP=false이면 코드와 상관없이 /mcp가 꺼집니다.setMcp(false), env에서는 AKAN_MCP=false입니다.setMcp((options) => ({ … }))는 env.server.*의 서버 옵션을 받으므로, 부팅 때 정해지는 값에 씁니다. libs/shared가 auth를 이렇게 만듭니다./mcp를 마운트할지 정합니다. env에 false나 0을 주면 코드와 상관없이 꺼집니다.true나 1일 때만 켭니다.aud도 함께 바뀝니다.serverInfo.version으로 보고됩니다. OpenAPI 문서와 같은 자리표시자입니다.nextCursor로 나머지를 받습니다.shallow는 중첩 모델의 이름만, full은 전부 인라인, none은 생략입니다.tools/call, resources/read, prompts/get에 대한 호출자별 예산이며, 프로세스마다 셉니다.Retry-After와 함께 429로 답합니다. 목록 조회는 세지 않고, 레플리카가 N개면 예산도 N개입니다.outputSchema: "none"이면 text block은 유지됩니다. legacyTextBlock 값과 상관없습니다. 클라이언트는 선언된 스키마가 있을 때만 structuredContent를 읽기 때문입니다.query나 mutation은 툴로 게시됩니다:startTask입니다..param(), .search(), .body() 인자를 한 객체로 모읍니다. .search() 인자는 선택입니다..desc()입니다.readOnlyHint, remove…나 delete… mutation에는 destructiveHint가 붙습니다..desc()가 없는 툴은 지저분한 게 아니라 고장 난 툴입니다..arg()의 설명이 input schema에 그대로 실립니다.slice()의 guards 맵으로 게시됩니다. 이름 있는 슬라이스는 그 맵을 물려받지 않으므로, 자기 가드를 직접 적어야 게시됩니다.guards.root로 막고, mcp: { root: false }로 뺍니다.guards.get과 mcp: { get: false }를 따르며, lightTask는 게시되지 않습니다.guards.cru와 mcp: { cru: false }를 따르고, create 같은 동사별 키로 따로 정할 수도 있습니다.init({ guards, mcp })만 봅니다.mcp: false를 씁니다. slice()에서는 guards와 같은 키의 맵입니다:mcp: false는 권한이 아니라 큐레이션입니다. 항목을 선반에서 뺄 뿐, 가드와 HTTP는 그대로입니다.slice()의 맵은 guards와 키가 똑같습니다. root, get, cru, create, update, remove이고, 닿는 범위도 루트 슬라이스와 생성된 CRUD로 같습니다.mcp: false라고 쓰면 전부 꺼집니다. root, get, cru로 펼쳐지고, create, update, remove는 cru를 물려받습니다.<model>과 <model>List… 조회에만 붙습니다. insight는 가리킬 대상이 없는 집계값이고, 커스텀 엔드포인트는 툴은 갖지만 템플릿은 받지 않습니다.…/list입니다. 그 자리는 슬라이스 키의 몫이기 때문입니다.lightTask는 툴도 URI도 없습니다.

queryKey는 모델의 filter 이름 하나를 받지만, args는 Any라 스키마에서 빠지고 값을 보내면 거부됩니다. 에이전트가 filter 인자까지 넘겨야 한다면 이름 있는 filter 슬라이스를 선언하세요..prompt(name, description)으로 선언하면, 사용자가 슬래시 커맨드로 실행하고 모델은 그 페이지가 불러온 데이터를 받습니다:<Agent.Guide> 문구는 MCP에 전달되지 않습니다..param()은 필수, .search()는 선택이고, desc가 인자 설명이 됩니다.Comma-separated list.가 붙고, ID, Int, enum 값은 페이지 자신의 선언으로 검증합니다.^[A-Za-z0-9_-]{1,64}$에 맞고 모든 페이지에서 겹치지 않아야 하며, prompts/list는 .prompt()가 있는 페이지를 모두 나열합니다.prompt() 빌더가 없고, Msg는 공개 API가 아닙니다.prompts/get은 페이지 body(root layout, layout, 그다음 render)를 호출자의 bearer 토큰으로 RSC worker에서 실행합니다. 렌더링은 하지 않고 클라이언트 컴포넌트도 돌지 않습니다. 페이지가 보낸 fetch.* 조회 하나하나가 응답이 됩니다:akan:// URI입니다. 커스텀 조회는 akan://<toolKey>?args입니다.project와 page의 lightProject처럼 같은 도큐먼트를 두 모양으로 읽으면, 더 큰 쪽 하나만 첨부합니다.401 인증 챌린지입니다. 클라이언트는 포기하지 않고 로그인하러 갑니다.router.notFound(), 또는 읽는 도큐먼트가 없다promptBudget에 맞게 잘립니다. 기본값은 60,000자이고, 큰 목록부터 자르며 Attached the first N of M rows of <key>; call it for the rest. 안내가 붙습니다. 단일 도큐먼트는 자르지 않습니다.mcp: false와 Person도 그대로 적용됩니다.web: false에는 RSC worker가 없기 때문입니다.report는 아무것도 하지 않으므로, 같은 서비스가 HTTP, 웹소켓, 테스트에서 그대로 동작합니다:Accept: text/event-stream과 _meta.progressToken을 모두 보내야 하며, 스트리밍은 tools/call에만 있습니다.exec을 프레임워크가 멈추지는 못합니다. 기다리는 쪽 없이 끝까지 실행됩니다.McpProgress.streaming은 누군가 읽는 동안 true입니다. 만들기 비싼 메시지는 그때만 조립하세요.Self, account 미들웨어는 브라우저 호출과 똑같이 동작합니다. 다른 점은 하나, cookie 헤더를 입구에서 버리므로 받는 자격 증명은 Authorization 헤더뿐입니다.static scope를 선언합니다. 이 값이 가드가 호출자의 목록에서 항목을 숨길 수 있는지 정합니다:/.well-known/oauth-authorization-server/mcp를 그 issuer에 연결합니다. issuer를 지정하는 순간 /mcp가 토큰을 요구합니다.AKAN_MCP_AUTH_SERVERSWWW-Authenticate와 함께 401을 받고, 지정 전에는 가드가 거절한 호출만 받습니다. 어느 쪽이든 클라이언트는 툴이 없다고 결론짓지 않고 로그인합니다.insufficient_scope는 AKAN_MCP_SCOPES를 설정했을 때만 검사합니다. libs/shared를 마운트한 앱이 직접 발급하는 토큰에는 scope claim이 없으므로, 그런 앱에 설정하면 그 토큰이 모두 403으로 거절됩니다.aud가 없는 토큰은 issuer가 지정된 뒤부터 거부되고, 지정되지 않은 동안은 통과합니다. 다른 리소스를 가리키는 aud는 항상 거부됩니다.setMcp()로 auth.verify를 넘겨야 위조 토큰이 익명 호출자로 읽히지 않고 거부됩니다. libs/shared는 자기 토큰에 이렇게 합니다.libs/shared를 마운트한 앱에서 에이전트가 토큰을 받는 과정은 에이전트를 위한 OAuth에서 단계별로 다룹니다.MCP catalogue: tools=… 아래에 거부된 엔드포인트마다 이유가 한 줄씩 나오는데, 둘 다 기본 로그 레벨보다 낮으니 AKAN_PUBLIC_LOG_LEVEL=verbose로 켜세요..of()에 모델의 .desc()를 적으세요. 생성된 CRUD 툴은 "Get X" 뒤에 그 설명을 붙이고, 루트 목록과 insight는 .of()의 라벨과 설명을 그대로 씁니다. 이 항목들이 가질 수 있는 문구는 그것뿐입니다.$ref를 금지하므로, 항목마다 언급한 모델의 스키마를 통째로 인라인하고 그 목록 전체를 접속하는 에이전트마다 다시 보냅니다. 시그널별 MCP catalogue cost: 줄이 바이트가 어디에 쓰였는지 알려 주고, 보통 mcp: { cru: false }가 가장 큰 효과를 냅니다.Unknown argument "x"., 없는 도큐먼트는 No <model> found for the arguments given.로 답합니다. 진짜 장애만 서버가 실패했다고 답합니다.field.visual을 쓰세요. 모든 MCP 결과와 readable schema에서 함께 빠지므로 둘이 어긋나지 않습니다.