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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
node_modules/
dist/

.projects/cache
.projects/vault
.env
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ Example:
bitrefill --json search-products --query "Amazon" --per_page 1 | jq '.products[0].name'
```

### LLM context (`llm-context`)

Generates Markdown from the MCP `tools/list` response: tool names, descriptions, parameter tables, JSON Schema, example `bitrefill …` invocations, and example MCP `tools/call` payloads. Intended for **CLAUDE.md**, **Cursor** rules, or **`.github/copilot-instructions.md`**.

- **stdout** by default, or **`-o` / `--output <file>`** to write a file.
- Uses the same auth as other commands (`--api-key`, `BITREFILL_API_KEY`, or OAuth).
- The generated **Connection** line shows a redacted MCP URL (`…/mcp/<API_KEY>`), not your real key.

```bash
export BITREFILL_API_KEY=YOUR_API_KEY
bitrefill llm-context -o BITREFILL-MCP.md
# or: bitrefill llm-context > BITREFILL-MCP.md
```

### Examples

```bash
Expand All @@ -83,6 +97,9 @@ bitrefill --help

# Clear stored credentials
bitrefill logout

# Export tool docs for coding agents (see "LLM context" above)
bitrefill llm-context -o BITREFILL-MCP.md
```

## Development
Expand Down
26 changes: 26 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
type OutputFormatter,
} from './output.js';
import { buildOptionsForTool, parseToolArgs } from './tools.js';
import { generateLlmContextMarkdown } from './llm-context.js';

/** Subcommands defined by the CLI; MCP tools with the same name are skipped. */
const RESERVED_TOOL_NAMES = new Set(['logout', 'llm-context']);

const BASE_MCP_URL = 'https://api.bitrefill.com/mcp';
const CALLBACK_PORT = 8098;
Expand Down Expand Up @@ -330,8 +334,30 @@ async function main(): Promise<void> {
}
});

program
.command('llm-context')
.description(
'Emit MCP tools reference as Markdown (for CLAUDE.md, Cursor rules, Copilot instructions)'
)
.option(
'-o, --output <file>',
'Write Markdown to a file instead of stdout'
)
.action((opts: { output?: string }) => {
const md = generateLlmContextMarkdown(tools, {
mcpUrl,
programName: program.name(),
});
if (opts.output) {
fs.writeFileSync(opts.output, md, 'utf-8');
} else {
process.stdout.write(md);
}
});

// Register each MCP tool as a subcommand
for (const tool of tools) {
if (RESERVED_TOOL_NAMES.has(tool.name)) continue;
const sub = program
.command(tool.name)
.description(tool.description ?? '');
Expand Down
81 changes: 81 additions & 0 deletions src/llm-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest';
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import { generateLlmContextMarkdown } from './llm-context.js';

describe('generateLlmContextMarkdown', () => {
it('includes tool name, description, schema, CLI and MCP examples', () => {
const tools: Tool[] = [
{
name: 'search_products',
description: 'Search catalog.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
limit: {
type: 'integer',
description: 'Max results',
},
},
required: ['query'],
},
},
];

const md = generateLlmContextMarkdown(tools, {
mcpUrl: 'https://api.bitrefill.com/mcp/test',
programName: 'bitrefill',
});

expect(md).toContain('# Bitrefill MCP — LLM context');
expect(md).toContain('## Connection');
expect(md).toContain('https://api.bitrefill.com/mcp/<API_KEY>');
expect(md).toContain('### `search_products`');
expect(md).toContain('Search catalog.');
expect(md).toContain('| `query` |');
expect(md).toContain('#### Input schema (JSON Schema)');
expect(md).toContain('"type": "object"');
expect(md).toContain('#### Example: CLI');
expect(md).toContain('bitrefill search_products');
expect(md).toContain('--query');
expect(md).toContain('#### Example: MCP `tools/call`');
expect(md).toContain('"method": "tools/call"');
expect(md).toContain('"name": "search_products"');
});

it('sorts tools by name', () => {
const tools: Tool[] = [
{
name: 'zebra',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'alpha',
inputSchema: { type: 'object', properties: {} },
},
];

const md = generateLlmContextMarkdown(tools);
expect(md.indexOf('`alpha`')).toBeLessThan(md.indexOf('`zebra`'));
});

it('includes output schema when present', () => {
const tools: Tool[] = [
{
name: 't',
inputSchema: { type: 'object', properties: {} },
outputSchema: {
type: 'object',
properties: { ok: { type: 'boolean' } },
},
},
];

const md = generateLlmContextMarkdown(tools);
expect(md).toContain('#### Output schema (JSON Schema)');
expect(md).toContain('"ok"');
});
});
235 changes: 235 additions & 0 deletions src/llm-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { JsonSchemaProperty } from './tools.js';

/** Public MCP base; paths under it may include an API key segment — never echo secrets in docs. */
const BITREFILL_MCP_PUBLIC_BASE = 'https://api.bitrefill.com/mcp';

export interface GenerateLlmContextOptions {
/** MCP server URL used for this session (shown in the header; API key segment is redacted). */
mcpUrl?: string;
/** CLI program name (default `bitrefill`). */
programName?: string;
}

function sanitizeMcpUrlForDocs(url: string): string {
if (url === BITREFILL_MCP_PUBLIC_BASE) return url;
if (url.startsWith(`${BITREFILL_MCP_PUBLIC_BASE}/`)) {
return `${BITREFILL_MCP_PUBLIC_BASE}/<API_KEY>`;
}
return url;
}

function sortedPropertyEntries(tool: Tool): [string, JsonSchemaProperty][] {
const schema = tool.inputSchema as {
properties?: Record<string, JsonSchemaProperty>;
};
if (!schema.properties) return [];
return Object.entries(schema.properties).sort(([a], [b]) =>
a.localeCompare(b)
);
}

/** JSON.stringify for use as a shell argument (bash/zsh). */
function shellArgFromString(value: string): string {
return JSON.stringify(value);
}

function exampleArgumentValue(prop: JsonSchemaProperty): string {
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
const first = prop.enum[0];
return typeof first === 'string'
? shellArgFromString(first)
: String(first);
}
const t = prop.type;
switch (t) {
case 'number':
case 'integer':
return '1';
case 'boolean':
return 'true';
case 'array':
return `'[]'`;
case 'object':
return `'{}'`;
case 'string':
default:
return shellArgFromString('example');
}
}

function buildCliExample(
tool: Tool,
programName: string,
entries: [string, JsonSchemaProperty][]
): string {
if (entries.length === 0) return `${programName} ${tool.name}`;

const schema = tool.inputSchema as { required?: string[] };
const required = new Set(schema.required ?? []);

const parts: string[] = [`${programName} ${tool.name}`];
for (const [name, prop] of entries) {
if (!required.has(name)) continue;
parts.push(`--${name} ${exampleArgumentValue(prop)}`);
}
for (const [name, prop] of entries) {
if (required.has(name)) continue;
parts.push(`--${name} ${exampleArgumentValue(prop)}`);
break;
}
return parts.join(' ');
}

function buildMcpToolsCallExample(
tool: Tool,
entries: [string, JsonSchemaProperty][]
): string {
const args: Record<string, unknown> = {};
const schema = tool.inputSchema as { required?: string[] };
const required = new Set(schema.required ?? []);

for (const [name, prop] of entries) {
if (!required.has(name)) continue;
args[name] = exampleJsonValue(prop);
}
if (Object.keys(args).length === 0 && entries.length > 0) {
const [name, prop] = entries[0];
args[name] = exampleJsonValue(prop);
}

return JSON.stringify(
{
method: 'tools/call',
params: {
name: tool.name,
arguments: args,
},
},
null,
2
);
}

function exampleJsonValue(prop: JsonSchemaProperty): unknown {
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
return prop.enum[0];
}
const t = prop.type;
switch (t) {
case 'number':
case 'integer':
return 1;
case 'boolean':
return true;
case 'array':
return [];
case 'object':
return {};
case 'string':
default:
return 'example';
}
}

function formatParameterTable(
tool: Tool,
entries: [string, JsonSchemaProperty][]
): string {
if (entries.length === 0) {
return '_No parameters._\n';
}

const schema = tool.inputSchema as { required?: string[] };
const required = new Set(schema.required ?? []);

const rows = entries.map(([name, prop]) => {
const req = required.has(name) ? 'yes' : 'no';
const ty = prop.type ?? '—';
const desc = (prop.description ?? '—').replace(/\|/g, '\\|');
return `| \`${name}\` | ${ty} | ${req} | ${desc} |`;
});

return [
'| Name | Type | Required | Description |',
'| --- | --- | --- | --- |',
...rows,
'',
].join('\n');
}

/**
* Markdown describing MCP tools (from `tools/list`): names, descriptions,
* parameter tables, JSON Schema, CLI examples, and `tools/call` JSON.
*/
export function generateLlmContextMarkdown(
tools: Tool[],
options?: GenerateLlmContextOptions
): string {
const programName = options?.programName ?? 'bitrefill';
const sorted = [...tools].sort((a, b) => a.name.localeCompare(b.name));

const lines: string[] = [
'# Bitrefill MCP — LLM context',
'',
'Generated by `' +
programName +
' llm-context`. Add this to **CLAUDE.md**, **Cursor rules**, or **`.github/copilot-instructions.md`** so agents know how to use the Bitrefill API via MCP or this CLI.',
'',
];

if (options?.mcpUrl) {
lines.push('## Connection');
lines.push('');
lines.push(
`- MCP URL used for this run: \`${sanitizeMcpUrlForDocs(options.mcpUrl)}\` (override with \`MCP_URL\` or \`--api-key\` / \`BITREFILL_API_KEY\`).`
);
lines.push('');
}

lines.push('## Tools');
lines.push('');

for (const tool of sorted) {
const entries = sortedPropertyEntries(tool);
lines.push(`### \`${tool.name}\``);
lines.push('');
lines.push(tool.description?.trim() || '_No description._');
lines.push('');
lines.push('#### Parameters');
lines.push('');
lines.push(formatParameterTable(tool, entries));
lines.push('#### Input schema (JSON Schema)');
lines.push('');
lines.push('```json');
lines.push(JSON.stringify(tool.inputSchema ?? {}, null, 2));
lines.push('```');
lines.push('');

if (tool.outputSchema) {
lines.push('#### Output schema (JSON Schema)');
lines.push('');
lines.push('```json');
lines.push(JSON.stringify(tool.outputSchema, null, 2));
lines.push('```');
lines.push('');
}

lines.push('#### Example: CLI');
lines.push('');
lines.push('```bash');
lines.push(buildCliExample(tool, programName, entries));
lines.push('```');
lines.push('');
lines.push('#### Example: MCP `tools/call`');
lines.push('');
lines.push('```json');
lines.push(buildMcpToolsCallExample(tool, entries));
lines.push('```');
lines.push('');
lines.push('---');
lines.push('');
}

return lines.join('\n');
}
Loading