서비스 MCP 구현
서비스 MCP는 쇼핑, 구독, 금융 같은 서비스가 직접 만들고 호스팅합니다. 서비스는 견적을 내고 intent만 만듭니다.
돈은 사용자의 지갑 MCP(pay_intent)가 움직이고, 나중 청구는 사용자가 승인한 권한 안에서만 됩니다.
템플릿으로 시작
섹션 제목: “템플릿으로 시작”생성기는 GitHub Packages에 있습니다. npm init 전에 business@pouchi.app으로 패키지 접근 권한을 요청하세요. 사용자 ~/.npmrc에 @shard-lab:registry=https://npm.pkg.github.com과 //npm.pkg.github.com/:_authToken=${NC_WAAS_PACKAGES_TOKEN}을 설정하고, 환경 변수에 접근 권한이 있는 read:packages 토큰을 넣습니다. 토큰 값은 파일이나 채팅에 쓰지 않습니다. 프로젝트의 .npmrc는 생성기를 실행한 뒤에 만들어집니다.
npm init @shard-lab/agentpay-service acme-news -- --name "Acme 뉴스"cd acme-newsnpm installnpm test # 테스트 게이트웨이로 실행. 체인, 네트워크 없음디렉터리acme-news/
디렉터리src/
- service.mjs 도구, intent, 서비스 청구
디렉터리test/
- service.test.mjs
- server.mjs 환경 변수를 읽어 실행
- Dockerfile
- .env.example
- .npmrc GitHub Packages (토큰은 환경 변수)
템플릿에는 도구 네 개가 있습니다: plans(가격), buy(단건 구매 intent), subscribe(월 구독 intent), cancel(해지).
단건 구매 intent
섹션 제목: “단건 구매 intent”한 번, 정확한 상품과 가격입니다. 권한(mandate)의 조건이 eq로 상품과 가격을 고정합니다.
const it = kit.intent({ action: "purchase", summary: "단건 리포트 3,000원", spend_okrw: 3000, calls: [approve(3000), pay(product)], // 지금 실행할 호출 (approve + 결제 함수) mandate: { key: "acme-news:buy:1", label: "단건 리포트", total_okrw: 3000, per_okrw: 3000, approve_cap_okrw: 3000, calls: 1, days: 1, permissions: [{ target: MERCHANT, signature: "pay(bytes32,uint256,uint256)", predicates: [{ arg: 1, op: "eq", uint: "1" }, { arg: 2, op: "eq", okrw: 3000 }] }] }});return { intent_url: it.intent_url }; // 에이전트는 이 URL을 pay_intent에 넘깁니다구독: 서비스 청구
섹션 제목: “구독: 서비스 청구”첫 결제는 intent로 지금 실행하고, 다음 결제는 서비스가 일정에 맞춰 청구합니다. merchant_charges: true와 주기 한도(period)를 넣습니다.
mandate: { key: "acme-news:plan", label: "Acme 뉴스 월 구독", total_okrw: 9900 * 12, per_okrw: 9900, approve_cap_okrw: 9900, calls: 12, days: 360, merchant_charges: true, period: { seconds: 30 * 86400, max_runs: 1 }, // 30일마다 최대 1회 permissions: [{ target: MERCHANT, signature: "pay(bytes32,uint256,uint256)", predicates: [{ arg: 1, op: "eq", uint: "2" }, { arg: 2, op: "lte", okrw: 9900 }] }]}-
사용자가 권한을 승인하면, 게이트웨이가 그 권한 전용 자격 증명을 서비스의
/agentpay/mandates로 보냅니다. 에이전트는 이 값을 보지 못합니다. -
키트가 게이트웨이에 서명된 조회로 자격 증명을 확인하고
onMandate를 부릅니다. 서비스는 이것을 카드 토큰처럼 암호화해 저장합니다.async onMandate({ credential, intentId }) {await db.subscriptions.attachCredential(intentId, encrypt(credential));} -
청구할 때가 되면
kit.merchant.charge를 부릅니다. 요청마다 HMAC 서명이 붙고, 같은charge_id는 재시도해도 한 번만 실행됩니다.const r = await kit.merchant.charge(credential, {charge_id: `${sub.id}-w${window}`, // 주기 창마다 하나summary: "월 구독 2회차", spend_okrw: 9900,calls: [approve(9900), pay(product)]});// r.status === "paid" 또는 r.error: PERIOD_LIMIT, OVER_LIMIT, OUTSIDE_MANDATE, MANDATE_REVOKED ... -
사용자가 해지하면
kit.merchant.release(credential)로 청구 권한을 반납합니다. 체인 권한은 사용자가 지갑의revoke_mandate로 끕니다.
테스트
섹션 제목: “테스트”@shard-lab/agentpay-service-kit/testing의 테스트 게이트웨이는 실제 게이트웨이와 같은 HTTP 계약으로 승인, 자격 증명 전달, 서비스 청구를 흉내 냅니다. 체인과 Pouchi는 쓰지 않습니다.
const gw = await startTestGateway({ service: { id: "acme-news", origin, contracts: [MERCHANT], merchantCharges: true }, asset: TOKEN });const out = await mcp.call("subscribe", { months: 3 });const r = await gw.approve(out.intent_url); // 사용자가 승인한 것처럼gw.advance(PERIOD); // 다음 주기로const [charged] = await svc.chargeDue(); // 서비스 청구assert.equal(charged.status, "paid");등록과 배포
섹션 제목: “등록과 배포”- 앱 운영자에게 서비스를 등록합니다:
id,name,origin, 나중 청구 허용 여부, 컨트랙트 주소. PUBLIC_URL을 등록한 origin과 똑같이 맞춥니다. 지갑은 그 origin에서만 intent를 가져옵니다.- Docker 이미지로 띄웁니다.
앞서 설정한 NC_WAAS_PACKAGES_TOKEN을 빌드 터미널에 export한 상태로 실행합니다. BuildKit이 .npmrc와 토큰을 각각 임시 secret으로 전달하며, 토큰을 이미지나 빌드 인자에 넣지 않습니다.
docker build --secret id=npmrc,src=$HOME/.npmrc \ --secret id=packages_token,env=NC_WAAS_PACKAGES_TOKEN -t acme-news-mcp .docker run -d -p 8080:8080 --env-file .env acme-news-mcp환경 변수와 등록 절차는 배포와 실행에 있습니다.
규칙 요약
섹션 제목: “규칙 요약”| 규칙 | 이유 |
|---|---|
권한 key는 <서비스 id>:로 시작 |
다른 서비스의 권한과 섞이지 않게 |
permissions의 대상은 등록된 자기 컨트랙트만 |
금융 서비스가 쇼핑 컨트랙트 권한을 받지 못하게 |
지금 실행할 calls는 권한 안에 들어가야 함 |
승인한 것과 실행하는 것이 같도록 |
결제 토큰 approve는 권한 대상에게, approve_cap_okrw 이하로만 |
남는 허용량으로 나중에 빼 가지 못하게 |
| 키트에서 만든 intent는 생성 후 15분 뒤 만료, 처음 가져온 내용으로 고정 | 승인 뒤 바꿔치기 방지 |
주요 API는 서비스 키트 레퍼런스에 있습니다.