@pay2s/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/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ > **0.5.0 security migration:** OAuth is now owned by `https://api.pay2s.vn`. Upgrade backend, gateway, frontend and npm together; existing users must log in again. See `../pay2s-mcp/UPGRADE-0.5.0.md`. Older installation instructions below are historical and do not override that guide.
2
+
1
3
  # Pay2S MCP
2
4
 
3
5
  Kết nối Codex, Cursor và ứng dụng hỗ trợ MCP stdio với tài khoản Pay2S. Cần Node.js 22 trở lên trên máy chạy AI và máy chủ Pay2S MCP 0.4 đã đăng ký client `pay2s-cli`.
@@ -49,10 +51,11 @@ Tham khảo cấu hình stdio: https://learn.chatgpt.com/docs/extend/mcp?surface
49
51
 
50
52
  - Xem ngân hàng, giao dịch, thu chi, đơn và trạng thái thanh toán trong phạm vi được cấp.
51
53
  - Tạo link thanh toán khi được cấp quyền riêng.
52
- - Đề nghị thêm webhook; đề nghị bật/tắt ngân hàng hoặc webhook. AI trả đường dẫn để chủ tài khoản xem và xác nhận trên Pay2S. Gửi đề nghị chưa làm thay đổi trạng thái.
54
+ - Bật/tắt ngân hàng và webhook trực tiếp bằng `set_bank_status`, `set_webhook_status` khi được cấp quyền tương ứng. AI trả trạng thái thực tế ngay, không tạo đề nghị chờ xác nhận.
55
+ - Đề nghị tạo webhook mới trả đường dẫn để chủ tài khoản kiểm tra địa chỉ nhận trên Pay2S.
53
56
  - `--docs`: chỉ đọc tài liệu công khai, không đăng nhập.
54
57
 
55
- Kết nối cũ cần đăng nhập lại để chọn quyền mới. Ngân hàng đang tắt vẫn xuất hiện để đề nghị bật lại; truy vấn giao dịch vẫn cần ngân hàng hoạt động. Bật webhook không gửi lại giao dịch cũ.
58
+ Kết nối đã có `banks:manage` và `webhooks:manage` dùng ngay công cụ bật/tắt sau khi máy chủ cập nhật 0.4.1 và AI tải lại danh sách công cụ. Chỉ đăng nhập lại nếu chưa cấp hai quyền này. Ngân hàng đang tắt vẫn xuất hiện để bật lại; truy vấn giao dịch vẫn cần ngân hàng hoạt động. Bật webhook không gửi lại giao dịch cũ.
56
59
 
57
60
  ## Phiên đăng nhập
58
61
 
@@ -74,3 +77,5 @@ pay2s-mcp login --profile codex
74
77
  ```
75
78
 
76
79
  Cấu hình ứng dụng lúc thử bản này dùng `command = "pay2s-mcp"`, `args = ["--profile", "codex"]`. Không chạy `npx @pay2s/mcp` trước khi package đã xuất bản.
80
+
81
+ Máy chủ 0.4.2 bổ sung `create_webhook`: tạo ngay trong quyền `webhooks:request` đã cấp, không quay lại Pay2S duyệt. Gói npm hiện tại tương thích; khởi động lại kết nối để cập nhật danh sách công cụ.
package/bin/pay2s-mcp.mjs CHANGED
@@ -30,6 +30,6 @@ try {
30
30
  else await close();
31
31
  } catch (error) {
32
32
  // SDK/HTTP exceptions may contain response bodies; never print them or credentials.
33
- const safe = error.message?.startsWith('Profile ') ? error.message : error.code === 'EADDRINUSE' ? 'Port 14572 is busy. Finish the other Pay2S login and retry.' : 'Cannot connect to Pay2S. Check Node 22+, network, deployed MCP 0.4 and account access. Run login with the same --profile again.';
33
+ const safe = error.message?.startsWith('Profile ') ? error.message : error.code === 'EADDRINUSE' ? 'Port 14572 is busy. Finish the other Pay2S login and retry.' : 'Cannot connect to Pay2S. Check Node 22+, network, deployed MCP and API backend 0.5 and account access. Run login with the same --profile again.';
34
34
  process.stderr.write(`${safe}\n`); await close(); process.exitCode = 1;
35
35
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
- {
2
- "name": "@pay2s/mcp",
3
- "version": "0.4.0",
4
- "description": "Connect AI tools to Pay2S through OAuth and MCP",
5
- "type": "module",
6
- "license": "UNLICENSED",
7
- "bin": { "pay2s-mcp": "bin/pay2s-mcp.mjs" },
8
- "files": ["bin/", "src/", "README.md"],
9
- "engines": { "node": ">=22" },
10
- "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
11
- "scripts": { "test": "node --test test/*.test.mjs" },
12
- "dependencies": { "@modelcontextprotocol/sdk": "1.31.0" }
13
- }
1
+ {
2
+ "name": "@pay2s/mcp",
3
+ "version": "0.5.0",
4
+ "description": "Connect AI tools to Pay2S through OAuth and MCP",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "bin": { "pay2s-mcp": "bin/pay2s-mcp.mjs" },
8
+ "files": ["bin/", "src/", "README.md"],
9
+ "engines": { "node": ">=22" },
10
+ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
11
+ "scripts": { "test": "node --test test/*.test.mjs" },
12
+ "dependencies": { "@modelcontextprotocol/sdk": "1.31.0" }
13
+ }
package/src/auth.mjs CHANGED
@@ -2,15 +2,31 @@ import { createServer } from 'node:http';
2
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
3
3
  import { execFile } from 'node:child_process';
4
4
  import { auth } from '@modelcontextprotocol/sdk/client/auth.js';
5
- export const ISSUER = 'https://mcp.pay2s.vn';
6
- export const RESOURCE = `${ISSUER}/mcp`;
5
+ export const ISSUER = 'https://api.pay2s.vn';
6
+ export const MCP_ORIGIN = 'https://mcp.pay2s.vn';
7
+ export const RESOURCE = `${MCP_ORIGIN}/mcp`;
8
+ export const AUTHORIZE = `${ISSUER}/api/v1/mcp/oauth/authorize`;
9
+ export const TOKEN = `${ISSUER}/api/v1/mcp/oauth/token`;
7
10
  export const REDIRECT = 'http://127.0.0.1:14572/callback';
8
11
  export const SCOPES = 'accounts:read transactions:read payments:read payments:create webhooks:read webhooks:request banks:manage webhooks:manage';
9
- export async function trustedFetch(input, init = {}) {
12
+ export async function trustedFetch(input, init = {}, transport = fetch) {
10
13
  const url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url);
11
- if (url.origin !== ISSUER || url.username || url.password) throw Error('Unexpected Pay2S server');
14
+ const method=(init.method || input.method || 'GET').toUpperCase();
15
+ const headers=new Headers(init.headers || input.headers);
16
+ const publicMetadata=url.origin===MCP_ORIGIN && ['/.well-known/oauth-protected-resource','/.well-known/oauth-protected-resource/mcp'].includes(url.pathname);
17
+ const authMetadata=url.origin===ISSUER && url.pathname==='/.well-known/oauth-authorization-server';
18
+ const resource=url.origin===MCP_ORIGIN && ['/mcp','/docs'].includes(url.pathname);
19
+ const token=url.href===TOKEN;
20
+ if(url.username||url.password||url.search||url.hash||!((method==='GET'&&(publicMetadata||authMetadata))||(method==='POST'&&resource&&headers.get('content-type')?.startsWith('application/json'))||(method==='POST'&&token&&headers.get('content-type')?.startsWith('application/x-www-form-urlencoded'))))throw Error('Unexpected Pay2S server or endpoint');
21
+ if(headers.has('authorization') && !(resource && url.pathname==='/mcp'))throw Error('Unexpected credential destination');
12
22
  const signal = init.signal ? AbortSignal.any([init.signal, AbortSignal.timeout(45000)]) : AbortSignal.timeout(45000);
13
- return fetch(input, { ...init, redirect: 'error', signal });
23
+ const response=await transport(input, { ...init, redirect: 'error', signal });
24
+ if(response.ok && (publicMetadata||authMetadata)){
25
+ const data=await response.clone().json();
26
+ if(publicMetadata && (data.resource!==RESOURCE||!Array.isArray(data.authorization_servers)||data.authorization_servers.length!==1||data.authorization_servers[0]!==ISSUER))throw Error('Unexpected authorization server');
27
+ if(authMetadata && (data.issuer!==ISSUER||data.authorization_endpoint!==AUTHORIZE||data.token_endpoint!==TOKEN||!data.code_challenge_methods_supported?.includes('S256')))throw Error('Unexpected authorization endpoints');
28
+ }
29
+ return response;
14
30
  }
15
31
  export function callbackHandler(state, accept) {
16
32
  let used = false;
@@ -36,7 +52,7 @@ export function callbackHandler(state, accept) {
36
52
  function openBrowser(url) {
37
53
  // Fixed HTTPS origin and URL serialization; no shell command interpolation.
38
54
  const target = new URL(url);
39
- if (target.origin !== ISSUER || target.pathname !== '/oauth/authorize') throw Error('Unexpected authorization URL');
55
+ if (target.origin !== ISSUER || target.pathname !== '/api/v1/mcp/oauth/authorize') throw Error('Unexpected authorization URL');
40
56
  const command = process.platform === 'win32' ? 'rundll32.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open';
41
57
  const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', target.href] : [target.href];
42
58
  return new Promise(resolve => execFile(command, args, { windowsHide: true }, () => resolve()));
@@ -47,13 +63,14 @@ export class Provider {
47
63
  get clientMetadata() { return { client_name: 'Pay2S MCP CLI', redirect_uris: [REDIRECT], token_endpoint_auth_method: 'none', grant_types: ['authorization_code','refresh_token'], response_types: ['code'], scope: SCOPES }; }
48
64
  clientInformation() { return { client_id: 'pay2s-cli', issuer: ISSUER }; }
49
65
  tokens() { const t = this.store.read()?.tokens; return t?.issuer === ISSUER ? t : undefined; }
50
- async saveTokens(tokens) { await this.store.save({ tokens, expires_at: Date.now() + Math.max(0, Number(tokens.expires_in || 0)) * 1000 }); }
66
+ async saveTokens(tokens) { if(tokens.issuer!==ISSUER)throw Error('Unexpected token issuer');await this.store.save({ tokens, expires_at: Date.now() + Math.max(0, Number(tokens.expires_in || 0)) * 1000 }); }
51
67
  async invalidateCredentials(scope) { if (['all','tokens'].includes(scope)) await this.store.save(undefined); if (['all','verifier'].includes(scope)) this.verifier = undefined; }
52
68
  saveCodeVerifier(value) { this.verifier = value; }
53
69
  codeVerifier() { if (!this.verifier) throw Error('Missing code verifier'); return this.verifier; }
54
70
  validateResourceURL(_url, resource) { if (resource !== RESOURCE) throw Error('Unexpected resource'); return Promise.resolve(new URL(RESOURCE)); }
55
71
  state() { this.nonce = randomBytes(32).toString('hex'); return this.nonce; }
56
72
  async redirectToAuthorization(url) {
73
+ if(url.origin!==ISSUER||url.pathname!=='/api/v1/mcp/oauth/authorize')throw Error('Unexpected authorization URL');
57
74
  if (!this.interactive) throw Error('Login required. Run npx -y @pay2s/mcp login with the same --profile.');
58
75
  this.callback = new Promise(resolve => { this.receive = resolve; });
59
76
  this.server = createServer(callbackHandler(this.nonce, this.receive));
package/src/proxy.mjs CHANGED
@@ -1,22 +1,22 @@
1
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
- import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { ListToolsRequestSchema, CallToolRequestSchema, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
6
- import { ISSUER, trustedFetch } from './auth.mjs';
7
- export async function proxy(provider, docs = false) {
8
- const transport = new StreamableHTTPClientTransport(new URL(`${ISSUER}/${docs ? 'docs' : 'mcp'}`), { authProvider: provider, fetch: trustedFetch });
9
- return connectProxy(transport, new StdioServerTransport());
10
- }
11
- export async function connectProxy(transport, localTransport) {
12
- const remote = new Client({ name: '@pay2s/mcp', version: '0.4.0' });
13
- await remote.connect(transport);
14
- const local = new Server({ name: 'pay2s', version: '0.4.0' }, { capabilities: { tools: {} }, instructions: remote.getInstructions() });
15
- // Serialize remote calls, including SDK token refresh. Never share a refresh token across processes.
16
- let tail = Promise.resolve();
17
- const enqueue = fn => { const next = tail.then(fn).catch(() => { throw Error('Pay2S request failed. Check your connection or sign in again.'); }); tail = next.catch(() => {}); return next; };
18
- local.setRequestHandler(ListToolsRequestSchema, request => enqueue(() => remote.listTools(request.params)));
19
- local.setRequestHandler(CallToolRequestSchema, request => enqueue(() => remote.callTool(request.params, CallToolResultSchema, { timeout: 60000 })));
20
- await local.connect(localTransport);
21
- return { async close() { await local.close(); await remote.close(); } };
22
- }
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { ListToolsRequestSchema, CallToolRequestSchema, CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
6
+ import { MCP_ORIGIN, trustedFetch } from './auth.mjs';
7
+ export async function proxy(provider, docs = false) {
8
+ const transport = new StreamableHTTPClientTransport(new URL(`${MCP_ORIGIN}/${docs ? 'docs' : 'mcp'}`), { authProvider: provider, fetch: trustedFetch });
9
+ return connectProxy(transport, new StdioServerTransport());
10
+ }
11
+ export async function connectProxy(transport, localTransport) {
12
+ const remote = new Client({ name: '@pay2s/mcp', version: '0.5.0' });
13
+ await remote.connect(transport);
14
+ const local = new Server({ name: 'pay2s', version: '0.5.0' }, { capabilities: { tools: {} }, instructions: remote.getInstructions() });
15
+ // Serialize remote calls, including SDK token refresh. Never share a refresh token across processes.
16
+ let tail = Promise.resolve();
17
+ const enqueue = fn => { const next = tail.then(fn).catch(() => { throw Error('Pay2S request failed. Check your connection or sign in again.'); }); tail = next.catch(() => {}); return next; };
18
+ local.setRequestHandler(ListToolsRequestSchema, request => enqueue(() => remote.listTools(request.params)));
19
+ local.setRequestHandler(CallToolRequestSchema, request => enqueue(() => remote.callTool(request.params, CallToolResultSchema, { timeout: 60000 })));
20
+ await local.connect(localTransport);
21
+ return { async close() { await local.close(); await remote.close(); } };
22
+ }