@sayren/mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -19,13 +19,17 @@ const METHODS = [
19
19
  "DELETE"
20
20
  ];
21
21
  /**
22
- * 돈이 나가는 쓰기 — 되돌릴 수 없으므로 에이전트에게 열지 않는다. 콘솔에서 사람이 한다.
23
- * 목록은 operationId(`{Controller}_{method}`)로 고정한다.
22
+ * 서버가 정책(`x-agent-policy`)을 싣지 않는 옛 api에만 쓰는 폴백 — 환불로 이어지는 쓰기를 막는다.
23
+ * 새 api는 문서의 정책이 원천이고 이 목록은 보지 않는다(승인 흐름으로 부를 수 있다).
24
24
  */
25
- const BLOCKED_OPERATIONS = {
25
+ const LEGACY_BLOCKED_OPERATIONS = {
26
26
  OrdersController_cancelBySeller: "셀러 직권취소는 PG 환불로 이어져 되돌릴 수 없다. 셀러 콘솔에서 처리한다",
27
27
  ClaimsController_approve: "클레임 승인은 환불로 이어져 되돌릴 수 없다. 셀러 콘솔에서 처리한다"
28
28
  };
29
+ /** 문서가 에이전트 정책을 싣는 api인가 — 하나라도 있으면 서버가 정책을 강제한다 */
30
+ function servesAgentPolicy(document) {
31
+ return Object.values(document.paths ?? {}).some((item) => Object.values(item).some((operation) => operation?.["x-agent-policy"] !== void 0));
32
+ }
29
33
  /**
30
34
  * 문서에서 에이전트가 쓸 수 있는 오퍼레이션만 고른다.
31
35
  * - `/v1/oauth2/*`: 토큰 발급 표면이다. PAT로 부를 일이 없다
@@ -34,6 +38,7 @@ const BLOCKED_OPERATIONS = {
34
38
  */
35
39
  function buildOperationIndex(document) {
36
40
  const operations = [];
41
+ const serverPolicy = servesAgentPolicy(document);
37
42
  for (const [path, item] of Object.entries(document.paths ?? {})) {
38
43
  if (!path.startsWith("/v1/") || path.startsWith("/v1/oauth2/")) continue;
39
44
  for (const method of METHODS) {
@@ -41,6 +46,7 @@ function buildOperationIndex(document) {
41
46
  if (!raw?.operationId) continue;
42
47
  const scopes = raw["x-required-scopes"] ?? [];
43
48
  if (scopes.length === 0 || raw["x-console-only"]) continue;
49
+ const policy = raw["x-agent-policy"];
44
50
  operations.push({
45
51
  operationId: raw.operationId,
46
52
  method,
@@ -49,7 +55,11 @@ function buildOperationIndex(document) {
49
55
  summary: raw.summary ?? raw.operationId,
50
56
  scopes,
51
57
  deprecated: raw.deprecated === true,
52
- blocked: BLOCKED_OPERATIONS[raw.operationId],
58
+ blocked: serverPolicy ? policy?.decision === "DENIED" ? policy.reason : void 0 : LEGACY_BLOCKED_OPERATIONS[raw.operationId],
59
+ approval: policy?.decision === "APPROVAL_REQUIRED" ? {
60
+ reason: policy.reason,
61
+ ...policy.condition ? { condition: policy.condition } : {}
62
+ } : void 0,
53
63
  raw
54
64
  });
55
65
  }
@@ -63,7 +73,8 @@ function formatOperationIndex(operations) {
63
73
  const flags = [
64
74
  op.scopes.join(","),
65
75
  op.deprecated ? "deprecated" : "",
66
- op.blocked ? "호출 불가" : ""
76
+ op.blocked ? "호출 불가" : "",
77
+ op.approval ? op.approval.condition ? "조건부 승인 필요" : "승인 필요" : ""
67
78
  ].filter(Boolean);
68
79
  const lines = byTag[op.tag] ?? [];
69
80
  lines.push(`${op.operationId} · ${op.method} ${op.path} · ${op.summary} [${flags.join(" · ")}]`);
@@ -102,6 +113,10 @@ function describeOperation(op, document) {
102
113
  requiredScopes: op.scopes,
103
114
  deprecated: op.deprecated || void 0,
104
115
  blocked: op.blocked,
116
+ approval: op.approval ? {
117
+ ...op.approval,
118
+ note: "부르면 서버가 실행하지 않고 승인 대기(202)로 둔다. 셀러가 콘솔에서 승인하면 서버가 그대로 실행한다. `dryRun: true`로 먼저 영향을 확인한다"
119
+ } : void 0,
105
120
  tool: op.blocked ? void 0 : op.method === "GET" ? "call_api_read" : "call_api_write",
106
121
  pathParams: parameters.filter((p) => p.in === "path").map((p) => p.name),
107
122
  query: parameters.filter((p) => p.in === "query").map((p) => ({
@@ -163,6 +178,109 @@ function truncateData(data, maxChars) {
163
178
  truncated: `응답 ${text$1.length}자 중 ${maxChars}자만 실었어요`
164
179
  };
165
180
  }
181
+ /** 같은 멱등키의 승인이 끝났을 때 상태별 안내 — 에이전트가 다음에 할 일 */
182
+ const DECIDED_GUIDE = {
183
+ SUCCEEDED: {
184
+ message: "이 멱등키의 요청은 셀러가 승인해서 이미 실행됐어요. 다시 실행하지 않았어요",
185
+ nextSteps: ["실행 결과는 `result`다. 같은 요청을 다시 보내지 않는다"]
186
+ },
187
+ FAILED: {
188
+ message: "이 멱등키의 요청은 승인 뒤 실행했지만 실패했어요",
189
+ nextSteps: ["`result.error`로 원인을 사용자에게 알린다", "고쳐서 다시 하려면 새 멱등키로 요청한다(같은 키는 이 결과만 돌려준다)"]
190
+ },
191
+ UNKNOWN: {
192
+ message: "이 멱등키의 요청은 승인 뒤 실행 결과를 확인하지 못했어요. 이미 실행됐을 수 있어요",
193
+ nextSteps: ["다시 보내지 않는다. 대상 리소스를 조회해 바뀌었는지 확인하고 사용자에게 알린다", "셀러에게 콘솔의 감사 로그에서 실행 여부를 확인해 달라고 전한다"]
194
+ },
195
+ REJECTED: {
196
+ message: "이 멱등키의 요청은 셀러가 거절했어요. 아무것도 바뀌지 않았어요",
197
+ nextSteps: ["거절 사유(`decisionNote`)를 전한다. 사용자가 원하면 새 멱등키로 다시 요청한다"]
198
+ },
199
+ EXPIRED: {
200
+ message: "이 멱등키의 요청은 승인 기한(24시간)이 지났어요. 아무것도 바뀌지 않았어요",
201
+ nextSteps: ["사용자가 원하면 새 멱등키로 다시 요청한다"]
202
+ }
203
+ };
204
+ /**
205
+ * 쓰기 결과를 에이전트가 읽을 형태로 바꾼다. 승인 대기(202 `APPROVAL_REQUIRED`)는 성공이 아니다 — 아직 아무것도
206
+ * 바뀌지 않았다는 것과 셀러에게 전할 링크, 상태를 확인할 도구를 함께 준다. 같은 멱등키의 승인이 이미 끝났으면
207
+ * (200 `APPROVAL_ALREADY_DECIDED`) `data.status`로 결과와 다음 할 일을 알린다. 에이전트 정책 거부(403)는 다시 시도하지
208
+ * 않게 알린다. 다른 202(비동기 접수)는 승인 대기가 아니다 — `meta.code`로만 가른다.
209
+ */
210
+ function describeWriteResult(result) {
211
+ if (result.ok && result.code === "APPROVAL_ALREADY_DECIDED") {
212
+ const approval = result.data ?? {};
213
+ const guide = DECIDED_GUIDE[approval.status ?? ""] ?? DECIDED_GUIDE.EXPIRED;
214
+ return {
215
+ isError: approval.status !== "SUCCEEDED",
216
+ body: {
217
+ status: `APPROVAL_${approval.status ?? "DECIDED"}`,
218
+ applied: approval.status === "SUCCEEDED" ? true : approval.status === "UNKNOWN" ? "unknown" : false,
219
+ message: guide?.message,
220
+ approvalId: approval.approvalId,
221
+ result: approval.result,
222
+ decisionNote: approval.decisionNote ?? void 0,
223
+ nextSteps: guide?.nextSteps
224
+ }
225
+ };
226
+ }
227
+ if (result.ok && result.code === "APPROVAL_REQUIRED") {
228
+ const pending = result.data ?? {};
229
+ if (pending.status === "EXECUTING") return {
230
+ isError: false,
231
+ body: {
232
+ status: "APPROVAL_EXECUTING",
233
+ applied: "unknown",
234
+ message: "셀러가 승인해서 서버가 이 요청을 실행하고 있어요",
235
+ approvalId: pending.approvalId,
236
+ nextSteps: ["같은 요청을 다시 보내지 않는다. 결과는 `get_approval_request`로 확인한다"]
237
+ }
238
+ };
239
+ return {
240
+ isError: false,
241
+ body: {
242
+ status: "APPROVAL_PENDING",
243
+ applied: false,
244
+ message: "승인 대기 중이에요. 아직 아무것도 바뀌지 않았어요. 셀러가 콘솔에서 승인하면 서버가 이 요청을 그대로 실행해요",
245
+ approvalId: pending.approvalId,
246
+ reason: pending.reason,
247
+ expiresAt: pending.expiresAt,
248
+ approvalUrl: pending.approvalUrl ?? void 0,
249
+ preview: pending.preview,
250
+ nextSteps: [pending.approvalUrl ? `사용자에게 승인 화면 링크를 전한다: ${pending.approvalUrl}` : "사용자에게 셀러 콘솔 › 설정 › 승인 요청에서 승인해 달라고 전한다", "같은 요청을 다시 보내지 않는다. 결과는 `get_approval_request`로 확인한다"]
251
+ }
252
+ };
253
+ }
254
+ if (result.ok && result.code === "DRY_RUN") return {
255
+ isError: false,
256
+ body: {
257
+ status: "DRY_RUN",
258
+ applied: false,
259
+ message: "실행하지 않았어요. 서버의 판정과 영향 미리보기예요",
260
+ ...result.data
261
+ }
262
+ };
263
+ if (!result.ok) {
264
+ if (result.error?.code === "AGENT_OPERATION_DENIED") return {
265
+ isError: true,
266
+ body: {
267
+ ...result,
268
+ nextSteps: ["에이전트는 이 작업을 할 수 없다. 다시 시도하지 말고 셀러에게 콘솔에서 직접 하도록 안내한다"]
269
+ }
270
+ };
271
+ return {
272
+ isError: true,
273
+ body: { ...result }
274
+ };
275
+ }
276
+ return {
277
+ isError: false,
278
+ body: {
279
+ status: result.status,
280
+ data: result.data
281
+ }
282
+ };
283
+ }
166
284
 
167
285
  //#endregion
168
286
  //#region src/user-agent.ts
@@ -223,6 +341,7 @@ var AdminApi = class {
223
341
  };
224
342
  if (input.body !== void 0) headers["content-type"] = "application/json";
225
343
  if (input.idempotencyKey) headers["idempotency-key"] = input.idempotencyKey;
344
+ if (input.dryRun) headers["x-sayren-dry-run"] = "1";
226
345
  const response = await this.fetchImpl(url, {
227
346
  method: input.method,
228
347
  headers,
@@ -238,6 +357,7 @@ var AdminApi = class {
238
357
  return response.ok ? {
239
358
  ok: true,
240
359
  status: response.status,
360
+ ...payload?.meta?.code ? { code: payload.meta.code } : {},
241
361
  data: payload?.data ?? payload
242
362
  } : {
243
363
  ok: false,
@@ -1067,8 +1187,21 @@ async function invoke(expected, input) {
1067
1187
  path: buildPath(op.path, input.pathParams ?? {}),
1068
1188
  query: input.query,
1069
1189
  body: input.body,
1070
- idempotencyKey: input.idempotencyKey
1190
+ idempotencyKey: input.idempotencyKey,
1191
+ dryRun: expected === "write" && input.dryRun === true
1071
1192
  });
1193
+ if (expected === "write") {
1194
+ const described = describeWriteResult(result);
1195
+ if (described.isError) return {
1196
+ ...text(described.body),
1197
+ isError: true
1198
+ };
1199
+ const { data: data$1, truncated: truncated$1 } = truncateData(described.body, MAX_RESPONSE_CHARS);
1200
+ return text(truncated$1 ? {
1201
+ ...data$1,
1202
+ truncated: truncated$1
1203
+ } : data$1);
1204
+ }
1072
1205
  if (!result.ok) return {
1073
1206
  ...text(result),
1074
1207
  isError: true
@@ -1095,13 +1228,14 @@ server.registerTool("call_api_read", {
1095
1228
  }, async (input) => invoke("read", input));
1096
1229
  server.registerTool("call_api_write", {
1097
1230
  title: "관리 API 변경",
1098
- description: `POST·PUT·PATCH·DELETE 오퍼레이션을 부른다. 스토어 데이터가 실제로 바뀐다. 사용자가 요청한 변경만 한다. \`describe_operation\`에서 \`idempotencyKey: true\`인 오퍼레이션은 키를 새로 만들어 넣고, 재시도할 때는 같은 키를 쓴다. 환불로 이어지는 오퍼레이션(직권취소·클레임 승인)은 막혀 있다. ${UNTRUSTED_NOTE}`,
1231
+ description: `POST·PUT·PATCH·DELETE 오퍼레이션을 부른다. 스토어 데이터가 실제로 바뀐다. 사용자가 요청한 변경만 한다. \`describe_operation\`에서 \`idempotencyKey: true\`인 오퍼레이션은 키를 새로 만들어 넣고, 재시도할 때는 같은 키를 쓴다. 바꾸기 전에 \`dryRun: true\`로 먼저 불러 대상·변경 전후·금액 영향과 승인 필요 여부를 사용자에게 보여 준다(서버가 실행하지 않는다). 환불·가격·할인처럼 승인이 필요한 쓰기는 서버가 승인 대기(\`APPROVAL_PENDING\`)로 두고, 셀러가 콘솔에서 승인하면 서버가 실행한다 — 결과 링크를 사용자에게 전하고 \`get_approval_request\`로 상태를 확인한다. 결제 설정·토큰 발급처럼 에이전트에게 거부된 작업은 다시 시도하지 않는다. ${UNTRUSTED_NOTE}`,
1099
1232
  inputSchema: {
1100
1233
  operationId: z.string().describe("`list_operations`가 준 operationId (GET 제외)"),
1101
1234
  pathParams: pathParamsInput,
1102
1235
  query: queryInput,
1103
1236
  body: z.unknown().optional().describe("요청 본문(JSON). `describe_operation`의 body 스키마를 따른다"),
1104
- idempotencyKey: z.string().max(255).optional().describe("멱등키. 재시도에는 같은 값을 쓴다")
1237
+ idempotencyKey: z.string().max(255).optional().describe("멱등키. 재시도에는 같은 값을 쓴다"),
1238
+ dryRun: z.boolean().optional().describe("true면 실행하지 않고 서버의 판정(ALLOW·APPROVAL_REQUIRED·DENIED)과 영향 미리보기만 받는다")
1105
1239
  },
1106
1240
  annotations: {
1107
1241
  readOnlyHint: false,
@@ -1110,6 +1244,29 @@ server.registerTool("call_api_write", {
1110
1244
  openWorldHint: false
1111
1245
  }
1112
1246
  }, async (input) => invoke("write", input));
1247
+ server.registerTool("get_approval_request", {
1248
+ title: "승인 요청 상태",
1249
+ description: "`call_api_write`가 승인 대기(`APPROVAL_PENDING`)로 돌려준 요청의 상태를 본다. `PENDING`은 아직 대기, `EXECUTING`은 승인 뒤 실행 중, `SUCCEEDED`·`FAILED`는 서버가 실행한 결과(`result`), `UNKNOWN`은 실행 결과를 확인하지 못함(이미 실행됐을 수 있으니 다시 보내지 말고 대상을 조회해 확인한다), `REJECTED`는 거절, `EXPIRED`는 기한(24시간) 지남이다. 승인은 셀러가 콘솔에서 한다 — 이 도구로 승인할 수 없다.",
1250
+ inputSchema: { approvalId: z.string().describe("승인 대기 응답의 approvalId") },
1251
+ annotations: {
1252
+ readOnlyHint: true,
1253
+ openWorldHint: false
1254
+ }
1255
+ }, async ({ approvalId }) => {
1256
+ const result = await adminApi.call({
1257
+ method: "GET",
1258
+ path: buildPath("/v1/approval-requests/{approvalId}", { approvalId })
1259
+ });
1260
+ if (!result.ok) return {
1261
+ ...text(result),
1262
+ isError: true
1263
+ };
1264
+ const { data, truncated } = truncateData(result.data, MAX_RESPONSE_CHARS);
1265
+ return text({
1266
+ data,
1267
+ ...truncated ? { truncated } : {}
1268
+ });
1269
+ });
1113
1270
  server.registerTool("verify_storefront", {
1114
1271
  title: "생성 결과 검증",
1115
1272
  description: "만들어진 프로젝트를 규칙과 대조한다. 결제가 조용히 실패하는 실수(브라우저 번들의 process.env, 클릭 시점 팝업 선오픈 누락, 결제 복귀 화면의 결과 확인 누락, 결과 확인 중 상태 조회 누락, 확정 배송비 미반영, 브라우저에 노출된 구매자 토큰 등)를 잡는다. 위반마다 고칠 자리(`file`·`line`)와 원인(`cause`)·수정 방법(`fix`)·예시 코드(`example`)·문서(`docUrl`)를 함께 준다. 규칙별 통과·위반은 `results`에 있다. 화면을 만든 뒤와 디자인을 고친 뒤에 부른다.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sayren-mcp": "./dist/index.mjs"
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "@modelcontextprotocol/sdk": "^1.22.0",
14
14
  "zod": "^4.6.5",
15
- "@sayren/store-sdk": "^0.13.0",
15
+ "@sayren/store-sdk": "^0.14.0",
16
16
  "@sayren/storefront-sdk": "^0.11.0"
17
17
  },
18
18
  "devDependencies": {
@@ -7,7 +7,7 @@
7
7
  "zod": "^4.6.5"
8
8
  },
9
9
  "versions": {
10
- "@sayren/store-sdk": "0.13.0",
10
+ "@sayren/store-sdk": "0.14.0",
11
11
  "@sayren/storefront-sdk": "0.11.0",
12
12
  "@sayren/ui": "0.1.0"
13
13
  }