Skip to content
Merged
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
34 changes: 28 additions & 6 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,57 @@ export class BazaarMcpServer {
tools: [
{
name: 'bazaar_search',
description: 'Search for resources in a Stellar Bazaar node',
description:
'Search for discovery resources indexed in a Stellar Bazaar node. ' +
'Cost: Free (no payment or tokens required). ' +
'Preconditions: Requires a reachable HTTP/HTTPS base URL of a Stellar Bazaar catalog node. ' +
'When NOT to use: Do NOT use this tool to execute paid API requests or purchase data (use `paid_call`). ' +
'Do NOT use this tool to query live SDEX order books or pricing feeds directly (use Lens REST/GraphQL APIs instead). ' +
'Do NOT pass specific API resource paths in the URL argument (provide only the root base URL of the node).',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The base URL of the Bazaar API',
description:
'The base URL of the Stellar Bazaar catalog node (e.g. "https://bazaar.example.com" or "http://localhost:3000"). ' +
'Format: Absolute HTTP or HTTPS URL without subpaths or trailing slashes.',
},
query: {
type: 'string',
description: 'Optional search query',
description:
'Optional search term to filter discovery catalog resources by keyword (e.g. "XLM price", "oracle"). ' +
'Format: UTF-8 plain text search string. If omitted, all indexed resources are returned.',
},
},
required: ['url'],
},
},
{
name: 'paid_call',
description: 'Make a paid API call to a Bazaar resource, handling the 402 loop automatically.',
description:
'Make an authenticated paid API call to an x402-enabled Stellar Bazaar resource endpoint. ' +
'Cost & Real Funds: SPENDS REAL CRYPTOGRAPHIC FUNDS (XLM/USDC) from the configured on-chain wallet. ' +
'Enforces a strict per-call spending limit (default max $1.00 USD equivalent via MCP_MAX_PAYMENT_PRICE). ' +
'Preconditions: Requires a funded Stellar signer with sufficient balance and valid trustlines. ' +
'Host Allow-List: Payments are restricted strictly to permitted hosts; calls to unlisted hosts will fail. ' +
'When NOT to use: Do NOT use this tool for free discovery searches (use `bazaar_search` instead). ' +
'Do NOT use if the agent does not intend to spend real funds or if estimated cost exceeds the per-call limit ($1.00). ' +
'Do NOT use for unverified, external, or non-Bazaar endpoints.',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'The full URL of the resource to fetch',
description:
'The full destination URL of the paid Bazaar resource endpoint (e.g. "https://api.example.com/v1/price"). ' +
'Format: Valid absolute HTTP or HTTPS URL pointing to an allowed payment host.',
},
method: {
type: 'string',
description: 'HTTP method (default: GET)',
description:
'HTTP request method for the target endpoint. ' +
'Format: Uppercase HTTP verb string ("GET" or "POST"). Default: "GET".',
},
},
required: ['url'],
Expand Down
29 changes: 28 additions & 1 deletion tests/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { BazaarMcpServer } from '../src/mcp/server';
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

describe('BazaarMcpServer', () => {
it('should map signature expiration ledger error to ERR_EXPIRED_AUTHORISATION', async () => {
Expand Down Expand Up @@ -120,4 +120,31 @@ describe('BazaarMcpServer', () => {
const parsedText = JSON.parse(response.content[0].text);
expect(parsedText.success).toBe(true);
});

it('should return clear tool descriptions with costs, preconditions, and argument formats in listTools', async () => {
const server = new BazaarMcpServer({ fetchWithPayment: vi.fn() });
let listHandler: any = null;
(server as any).server.setRequestHandler = (schema: any, fn: any) => {
if (schema === ListToolsRequestSchema) {
listHandler = fn;
}
};
(server as any).setupHandlers();

const result = await listHandler();
expect(result.tools).toHaveLength(2);

const searchTool = result.tools.find((t: any) => t.name === 'bazaar_search');
expect(searchTool.description).toContain('Cost: Free');
expect(searchTool.description).toContain('When NOT to use:');
expect(searchTool.inputSchema.properties.url.description).toContain('Format:');

const paidTool = result.tools.find((t: any) => t.name === 'paid_call');
expect(paidTool.description).toContain('SPENDS REAL CRYPTOGRAPHIC FUNDS');
expect(paidTool.description).toContain('$1.00');
expect(paidTool.description).toContain('Host Allow-List:');
expect(paidTool.description).toContain('When NOT to use:');
expect(paidTool.inputSchema.properties.url.description).toContain('Format:');
expect(paidTool.inputSchema.properties.method.description).toContain('Format:');
});
});
Loading