콘텐츠로 이동
요금문서EN

서비스 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-news
npm install
npm 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(해지).

한 번, 정확한 상품과 가격입니다. 권한(mandate)의 조건이 eq로 상품과 가격을 고정합니다.

src/service.mjs
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 }] }]
}
  1. 사용자가 권한을 승인하면, 게이트웨이가 그 권한 전용 자격 증명을 서비스의 /agentpay/mandates로 보냅니다. 에이전트는 이 값을 보지 못합니다.

  2. 키트가 게이트웨이에 서명된 조회로 자격 증명을 확인하고 onMandate를 부릅니다. 서비스는 이것을 카드 토큰처럼 암호화해 저장합니다.

    async onMandate({ credential, intentId }) {
    await db.subscriptions.attachCredential(intentId, encrypt(credential));
    }
  3. 청구할 때가 되면 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 ...
  4. 사용자가 해지하면 kit.merchant.release(credential)로 청구 권한을 반납합니다. 체인 권한은 사용자가 지갑의 revoke_mandate로 끕니다.

@shard-lab/agentpay-service-kit/testing의 테스트 게이트웨이는 실제 게이트웨이와 같은 HTTP 계약으로 승인, 자격 증명 전달, 서비스 청구를 흉내 냅니다. 체인과 Pouchi는 쓰지 않습니다.

test/service.test.mjs
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");
  1. 앱 운영자에게 서비스를 등록합니다: id, name, origin, 나중 청구 허용 여부, 컨트랙트 주소.
  2. PUBLIC_URL을 등록한 origin과 똑같이 맞춥니다. 지갑은 그 origin에서만 intent를 가져옵니다.
  3. 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는 서비스 키트 레퍼런스에 있습니다.