Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
25 changes: 25 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@x402scan/proxy": "1.0.0",
"@x402scan/solana-rpc": "0.0.0",
"@x402scan/app": "0.1.0",
"facilitators": "0.0.10",
"@x402scan/mcp": "0.2.1",
"@x402scan/siwx": "0.0.1",
"@x402scan/eslint-config": "0.0.0",
"@x402scan/typescript-config": "0.0.0",
"@x402scan/analytics-db": "1.0.0",
"@x402scan/partners-db": "1.0.0",
"@x402scan/scan-db": "1.0.0",
"prisma-client-1d8bcb17ac472d49b060ef3f01db4b30365f35bcce0282b975003196210c16f5": "6.19.0",
"@x402scan/transfers-db": "1.0.0",
"prisma-client-bfff2ef213b3a2f8bed96e55a92d5644e66afcab25ce177991a5c8039418c607": "6.19.0",
"@x402scan/neverthrow": "1.0.0",
"@x402scan/sync-alerts": "1.0.0",
"@x402scan/sync-analytics": "1.0.0",
"@x402scan/sync-transfers": "1.0.0"
},
"changesets": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ export const CreateInviteCodeButton = () => {
placeholder="WELCOME10 (auto-generated if empty)"
value={code}
onChange={e =>
setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))
setCode(
e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '')
)
}
/>
<p className="text-xs text-muted-foreground">
Expand Down Expand Up @@ -213,7 +215,9 @@ export const CreateInviteCodeButton = () => {
</div>
<Checkbox
checked={uniqueRecipients}
onCheckedChange={checked => setUniqueRecipients(checked === true)}
onCheckedChange={checked =>
setUniqueRecipients(checked === true)
}
/>
</div>

Expand Down
112 changes: 112 additions & 0 deletions packages/external/mcp/src/cli/commands/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { randomBytes } from 'crypto';

import {
successResponse,
errorResponse,
fromNeverthrowError,
outputAndExit,
type OutputFlags,
} from '@/cli/output';

import { buildRequest } from '@/server/tools/lib/request';
import { checkEndpoint } from '@/shared/operations';
import { safeParseResponse } from '@/shared/neverthrow/fetch';
import { getWalletOrExit, parseRequestInput } from './lib';

import type { GlobalFlags } from '@/types';
import type { JsonObject } from '@/shared/neverthrow/json/types';

const SURFACE = 'cli:check';

interface CheckArgs {
url: string;
method?: string;
body?: string;
headers?: string;
}

export async function checkCommand(
args: CheckArgs,
flags: GlobalFlags<OutputFlags>
): Promise<void> {
const { account } = await getWalletOrExit(flags);
const sessionId = randomBytes(16).toString('hex');

const input = parseRequestInput(SURFACE, args, flags);

const request = buildRequest({
input,
address: account.address,
sessionId,
});

const result = await checkEndpoint(SURFACE, request);

if (result.isErr()) {
outputAndExit(fromNeverthrowError(result), flags);
}

const value = result.value;

// Handle Response (non-ok HTTP response)
if (value instanceof Response) {
const response = value;
const parseResult = await safeParseResponse(SURFACE, response);
const details: JsonObject = { statusCode: response.status };
if (parseResult.isOk()) {
const { type } = parseResult.value;
if (type === 'json') {
details.body = parseResult.value.data;
} else if (type === 'text') {
details.body = parseResult.value.data;
} else {
details.bodyType = type;
}
}
outputAndExit(
errorResponse({
code: 'HTTP_ERROR',
message: `HTTP ${response.status}: ${response.statusText}`,
surface: SURFACE,
cause: 'http',
details,
}),
flags
);
}

// Handle CheckEndpointPaidResult
if ('requiresPayment' in value && value.requiresPayment) {
outputAndExit(
successResponse({
requiresPayment: true,
statusCode: value.statusCode,
routeDetails: value.routeDetails,
}),
flags
);
}

// Handle CheckEndpointFreeResult
if ('data' in value) {
outputAndExit(
successResponse({
requiresPayment: false,
statusCode: value.statusCode,
data: value.data,
}),
flags
);
}

// Fallback - shouldn't reach here
outputAndExit(
errorResponse({
code: 'GENERAL_ERROR',
message: 'Unexpected response format',
surface: SURFACE,
cause: 'unknown',
}),
flags
);
}
46 changes: 46 additions & 0 deletions packages/external/mcp/src/cli/commands/discover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
successResponse,
errorResponse,
outputAndExit,
type OutputFlags,
} from '@/cli/output';

import { discoverResources } from '@/shared/operations';

import type { GlobalFlags } from '@/types';

interface DiscoverArgs {
url: string;
}

export async function discoverCommand(
args: DiscoverArgs,
flags: GlobalFlags<OutputFlags>
): Promise<void> {
const result = await discoverResources('cli:discover', args.url);

if (result.isOk()) {
outputAndExit(
successResponse({
found: true,
origin: result.value.origin,
source: result.value.source,
...(result.value.usage ? { usage: result.value.usage } : {}),
data: result.value.data,
}),
flags
);
}

// Error case
outputAndExit(
errorResponse({
code: 'GENERAL_ERROR',
message: result.error.message,
surface: result.error.surface,
cause: result.error.cause,
details: { origin: result.error.origin },
}),
flags
);
}
133 changes: 133 additions & 0 deletions packages/external/mcp/src/cli/commands/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { randomBytes } from 'crypto';

import { x402Client, x402HTTPClient } from '@x402/core/client';
import { ExactEvmScheme } from '@x402/evm/exact/client';

import {
successResponse,
errorResponse,
fromNeverthrowError,
outputAndExit,
type OutputFlags,
} from '@/cli/output';

import { buildRequest } from '@/server/tools/lib/request';
import { DEFAULT_NETWORK } from '@/shared/networks';
import { tokenStringToNumber } from '@/shared/token';
import { safeParseResponse } from '@/shared/neverthrow/fetch';
import { safeGetPaymentSettlement } from '@/shared/neverthrow/x402';
import { createFetchWithPayment } from '@/shared/operations';
import { getWalletOrExit, parseRequestInput } from './lib';

import type { GlobalFlags } from '@/types';
import type { JsonObject } from '@/shared/neverthrow/json/types';

const SURFACE = 'cli:fetch';

interface FetchArgs {
url: string;
method?: string;
body?: string;
headers?: string;
}

export async function fetchCommand(
args: FetchArgs,
flags: GlobalFlags<OutputFlags>
): Promise<void> {
const { account } = await getWalletOrExit(flags);
const sessionId = randomBytes(16).toString('hex');

const input = parseRequestInput(SURFACE, args, flags);

// Set up x402 client
const coreClient = x402Client.fromConfig({
schemes: [
{ network: DEFAULT_NETWORK, client: new ExactEvmScheme(account) },
],
});

const client = new x402HTTPClient(coreClient);

const request = buildRequest({
input,
address: account.address,
sessionId,
});

const fetchWithPay = createFetchWithPayment(SURFACE, client);
const fetchResult = await fetchWithPay(request);

if (fetchResult.isErr()) {
outputAndExit(fromNeverthrowError(fetchResult), flags);
}

const { response, paymentPayload } = fetchResult.value;

if (!response.ok) {
const parseResult = await safeParseResponse(SURFACE, response);
const details: JsonObject = { statusCode: response.status };
if (parseResult.isOk()) {
const { type } = parseResult.value;
if (type === 'json') {
details.body = parseResult.value.data;
} else if (type === 'text') {
details.body = parseResult.value.data;
} else {
details.bodyType = type;
}
}
outputAndExit(
errorResponse({
code: 'HTTP_ERROR',
message: `HTTP ${response.status}: ${response.statusText}`,
surface: SURFACE,
cause: 'http',
details,
}),
flags
);
}

const parseResponseResult = await safeParseResponse(SURFACE, response);
if (parseResponseResult.isErr()) {
outputAndExit(fromNeverthrowError(parseResponseResult), flags);
}

const settlementResult = safeGetPaymentSettlement(SURFACE, client, response);

// Build response data
const data =
parseResponseResult.value.type === 'json'
? parseResponseResult.value.data
: parseResponseResult.value.type === 'text'
? parseResponseResult.value.data
: { type: parseResponseResult.value.type };

// Build metadata
const metadata =
settlementResult.isOk() || paymentPayload !== undefined
? {
...(paymentPayload !== undefined
? {
price: tokenStringToNumber(
paymentPayload.accepted.amount
).toLocaleString('en-US', {
style: 'currency',
currency: 'USD',
}),
}
: {}),
...(settlementResult.isOk()
? {
payment: {
success: settlementResult.value.success,
transactionHash: settlementResult.value.transaction,
},
}
: {}),
}
: undefined;

outputAndExit(successResponse(data, metadata), flags);
}
6 changes: 6 additions & 0 deletions packages/external/mcp/src/cli/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { fetchCommand } from './fetch';
export { checkCommand } from './check';
export { discoverCommand } from './discover';
export { walletInfoCommand, walletRedeemCommand } from './wallet';
export { reportErrorCommand } from './report-error';
export { serverCommand } from './server';
30 changes: 30 additions & 0 deletions packages/external/mcp/src/cli/commands/lib/get-wallet-or-exit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
fromNeverthrowError,
outputAndExit,
type OutputFlags,
} from '@/cli/output';
import { getWallet } from '@/shared/wallet';

import type { GlobalFlags } from '@/types';
import type { PrivateKeyAccount } from 'viem/accounts';

interface WalletInfo {
account: PrivateKeyAccount;
}

/**
* Get wallet or exit with error.
* This function always returns a valid wallet - if getting the wallet fails,
* it exits the process with an appropriate error.
*/
export async function getWalletOrExit(
flags: GlobalFlags<OutputFlags>
): Promise<WalletInfo> {
const walletResult = await getWallet();

if (walletResult.isErr()) {
outputAndExit(fromNeverthrowError(walletResult, 'WALLET_ERROR'), flags);
}

return walletResult.value;
}
2 changes: 2 additions & 0 deletions packages/external/mcp/src/cli/commands/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { getWalletOrExit } from './get-wallet-or-exit';
export { parseRequestInput } from './parse-request-input';
Loading
Loading