Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ cvm-context
ts-sdk
gateway-cli
proxy-cli
# sdk
# sdk

# per-developer MCP client config (local test rigs)
.mcp.json
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@
"author": "",
"license": "MIT",
"dependencies": {
"@contextvm/sdk": "^0.11.14",
"@contextvm/sdk": "^0.13.7",
"@modelcontextprotocol/sdk": "^1.27.1",
"json-schema-to-typescript": "15.0.4",
"nostr-tools": "^2.23.3",
"nostr-tools": "^2.23.9",
"xdg-basedir": "^5.1.0",
"zod": "^4.3.6"
},
Expand Down
127 changes: 38 additions & 89 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 79 additions & 2 deletions src/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
setCreateRemoteClientFactoryForTests,
} from './call.ts';
import { stripAnsi } from './test-utils.ts';
import { PAYMENT_REQUIRED_ERROR_CODE } from '@contextvm/sdk/payments/constants';

function captureConsoleOutput(render: () => void): string[] {
const output: string[] = [];
Expand Down Expand Up @@ -73,10 +74,24 @@ describe('parseCallArgs', () => {
expect(parsed.encryption).toBe(EncryptionMode.REQUIRED);
expect(parsed.isStateless).toBe(false);
expect(parsed.showServerDetails).toBe(false);
expect(parsed.unknownFlags).toEqual([]);
expect(parsed.paymentMode).toBe('transparent');
});

it('parses --payment-mode flag', () => {
const transparent = parseCallArgs(['server', '--payment-mode', 'transparent']);
expect(transparent.paymentMode).toBe('transparent');

const explicit = parseCallArgs(['server', '--payment-mode', 'explicit_gating']);
expect(explicit.paymentMode).toBe('explicit_gating');
});

it('rejects invalid --payment-mode flag', () => {
const invalid = parseCallArgs(['server', '--payment-mode', 'invalid_mode']);
expect(invalid.paymentMode).toBe('transparent'); // defaults to transparent
expect(invalid.unknownFlags).toEqual(['--payment-mode (invalid_mode)']);
});

it('parses server details flag explicitly', () => {
it('parses flags intermixed with kv pairs', () => {
const parsed = parseCallArgs(['weather', '--details']);

expect(parsed.server).toBe('weather');
Expand Down Expand Up @@ -594,6 +609,10 @@ Or pass a direct server identity in hex, npub, or nprofile format.]`);
listTools,
callTool,
},
transport: {
getServerToolsListEvent: () => undefined,
getServerInitializeEvent: () => undefined,
},
metadata: {},
close,
}) as never
Expand Down Expand Up @@ -833,6 +852,45 @@ Or pass a direct server identity in hex, npub, or nprofile format.]`);
resetCreateRemoteClientFactoryForTests();
});

it('throws ExplicitGatingError for -32042 in explicit_gating mode', async () => {
const listTools = vi.fn().mockResolvedValue({ tools: [] });
const mockError = new Error('Payment Required') as any;
mockError.code = PAYMENT_REQUIRED_ERROR_CODE;
mockError.data = { foo: 'bar' };
const callTool = vi.fn().mockRejectedValue(mockError);
const close = vi.fn().mockResolvedValue(undefined);

setCreateRemoteClientFactoryForTests(
vi.fn().mockResolvedValue({
client: { listTools, callTool },
metadata: {},
close,
}) as never
);

let error: any;
try {
await call(
'750682303c9f0ddad75941b49edc9d46e3ed306b9ee3335338a21a3e404c5fa3',
'read_media_file',
{},
{
privateKey: 'nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqj4xw9h',
paymentMode: 'explicit_gating',
}
);
} catch (e) {
error = e;
}

expect(error).toBeDefined();
expect(error.name).toBe('ExplicitGatingError');
expect(error.data).toEqual({ foo: 'bar' });
expect(close).toHaveBeenCalled();

resetCreateRemoteClientFactoryForTests();
});

it('prints extracted string values without JSON encoding', async () => {
const listTools = vi.fn().mockResolvedValue({
tools: [
Expand Down Expand Up @@ -882,4 +940,23 @@ Or pass a direct server identity in hex, npub, or nprofile format.]`);

resetCreateRemoteClientFactoryForTests();
});

describe('isPaymentRequiredError', () => {
it('returns true for an error with code -32042', () => {
const error = new Error('Payment Required') as any;
error.code = PAYMENT_REQUIRED_ERROR_CODE;
expect(__test__.isPaymentRequiredError(error)).toBe(true);
});

it('returns false for non-error objects', () => {
expect(__test__.isPaymentRequiredError({ code: PAYMENT_REQUIRED_ERROR_CODE })).toBe(false);
expect(__test__.isPaymentRequiredError(null)).toBe(false);
});

it('returns false for other error codes', () => {
const error = new Error('Other Error') as any;
error.code = -32601;
expect(__test__.isPaymentRequiredError(error)).toBe(false);
});
});
});
Loading
Loading