Skip to content

Commit 2d2a626

Browse files
authored
Merge pull request #4 from bitrefill/feat/llm-context
feat: llm-context command for agent Markdown
2 parents 5aa7a1c + 008960a commit 2d2a626

File tree

5 files changed

+362
-0
lines changed

5 files changed

+362
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
node_modules/
22
dist/
3+
4+
.projects/cache
5+
.projects/vault
36
.env

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ Example:
6363
bitrefill --json search-products --query "Amazon" --per_page 1 | jq '.products[0].name'
6464
```
6565

66+
### LLM context (`llm-context`)
67+
68+
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`**.
69+
70+
- **stdout** by default, or **`-o` / `--output <file>`** to write a file.
71+
- Uses the same auth as other commands (`--api-key`, `BITREFILL_API_KEY`, or OAuth).
72+
- The generated **Connection** line shows a redacted MCP URL (`…/mcp/<API_KEY>`), not your real key.
73+
74+
```bash
75+
export BITREFILL_API_KEY=YOUR_API_KEY
76+
bitrefill llm-context -o BITREFILL-MCP.md
77+
# or: bitrefill llm-context > BITREFILL-MCP.md
78+
```
79+
6680
### Examples
6781

6882
```bash
@@ -83,6 +97,9 @@ bitrefill --help
8397

8498
# Clear stored credentials
8599
bitrefill logout
100+
101+
# Export tool docs for coding agents (see "LLM context" above)
102+
bitrefill llm-context -o BITREFILL-MCP.md
86103
```
87104

88105
## Development

src/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import {
2828
type OutputFormatter,
2929
} from './output.js';
3030
import { buildOptionsForTool, parseToolArgs } from './tools.js';
31+
import { generateLlmContextMarkdown } from './llm-context.js';
32+
33+
/** Subcommands defined by the CLI; MCP tools with the same name are skipped. */
34+
const RESERVED_TOOL_NAMES = new Set(['logout', 'llm-context']);
3135

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

337+
program
338+
.command('llm-context')
339+
.description(
340+
'Emit MCP tools reference as Markdown (for CLAUDE.md, Cursor rules, Copilot instructions)'
341+
)
342+
.option(
343+
'-o, --output <file>',
344+
'Write Markdown to a file instead of stdout'
345+
)
346+
.action((opts: { output?: string }) => {
347+
const md = generateLlmContextMarkdown(tools, {
348+
mcpUrl,
349+
programName: program.name(),
350+
});
351+
if (opts.output) {
352+
fs.writeFileSync(opts.output, md, 'utf-8');
353+
} else {
354+
process.stdout.write(md);
355+
}
356+
});
357+
333358
// Register each MCP tool as a subcommand
334359
for (const tool of tools) {
360+
if (RESERVED_TOOL_NAMES.has(tool.name)) continue;
335361
const sub = program
336362
.command(tool.name)
337363
.description(tool.description ?? '');

src/llm-context.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, it, expect } from 'vitest';
2+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
3+
import { generateLlmContextMarkdown } from './llm-context.js';
4+
5+
describe('generateLlmContextMarkdown', () => {
6+
it('includes tool name, description, schema, CLI and MCP examples', () => {
7+
const tools: Tool[] = [
8+
{
9+
name: 'search_products',
10+
description: 'Search catalog.',
11+
inputSchema: {
12+
type: 'object',
13+
properties: {
14+
query: {
15+
type: 'string',
16+
description: 'Search query',
17+
},
18+
limit: {
19+
type: 'integer',
20+
description: 'Max results',
21+
},
22+
},
23+
required: ['query'],
24+
},
25+
},
26+
];
27+
28+
const md = generateLlmContextMarkdown(tools, {
29+
mcpUrl: 'https://api.bitrefill.com/mcp/test',
30+
programName: 'bitrefill',
31+
});
32+
33+
expect(md).toContain('# Bitrefill MCP — LLM context');
34+
expect(md).toContain('## Connection');
35+
expect(md).toContain('https://api.bitrefill.com/mcp/<API_KEY>');
36+
expect(md).toContain('### `search_products`');
37+
expect(md).toContain('Search catalog.');
38+
expect(md).toContain('| `query` |');
39+
expect(md).toContain('#### Input schema (JSON Schema)');
40+
expect(md).toContain('"type": "object"');
41+
expect(md).toContain('#### Example: CLI');
42+
expect(md).toContain('bitrefill search_products');
43+
expect(md).toContain('--query');
44+
expect(md).toContain('#### Example: MCP `tools/call`');
45+
expect(md).toContain('"method": "tools/call"');
46+
expect(md).toContain('"name": "search_products"');
47+
});
48+
49+
it('sorts tools by name', () => {
50+
const tools: Tool[] = [
51+
{
52+
name: 'zebra',
53+
inputSchema: { type: 'object', properties: {} },
54+
},
55+
{
56+
name: 'alpha',
57+
inputSchema: { type: 'object', properties: {} },
58+
},
59+
];
60+
61+
const md = generateLlmContextMarkdown(tools);
62+
expect(md.indexOf('`alpha`')).toBeLessThan(md.indexOf('`zebra`'));
63+
});
64+
65+
it('includes output schema when present', () => {
66+
const tools: Tool[] = [
67+
{
68+
name: 't',
69+
inputSchema: { type: 'object', properties: {} },
70+
outputSchema: {
71+
type: 'object',
72+
properties: { ok: { type: 'boolean' } },
73+
},
74+
},
75+
];
76+
77+
const md = generateLlmContextMarkdown(tools);
78+
expect(md).toContain('#### Output schema (JSON Schema)');
79+
expect(md).toContain('"ok"');
80+
});
81+
});

src/llm-context.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2+
import type { JsonSchemaProperty } from './tools.js';
3+
4+
/** Public MCP base; paths under it may include an API key segment — never echo secrets in docs. */
5+
const BITREFILL_MCP_PUBLIC_BASE = 'https://api.bitrefill.com/mcp';
6+
7+
export interface GenerateLlmContextOptions {
8+
/** MCP server URL used for this session (shown in the header; API key segment is redacted). */
9+
mcpUrl?: string;
10+
/** CLI program name (default `bitrefill`). */
11+
programName?: string;
12+
}
13+
14+
function sanitizeMcpUrlForDocs(url: string): string {
15+
if (url === BITREFILL_MCP_PUBLIC_BASE) return url;
16+
if (url.startsWith(`${BITREFILL_MCP_PUBLIC_BASE}/`)) {
17+
return `${BITREFILL_MCP_PUBLIC_BASE}/<API_KEY>`;
18+
}
19+
return url;
20+
}
21+
22+
function sortedPropertyEntries(tool: Tool): [string, JsonSchemaProperty][] {
23+
const schema = tool.inputSchema as {
24+
properties?: Record<string, JsonSchemaProperty>;
25+
};
26+
if (!schema.properties) return [];
27+
return Object.entries(schema.properties).sort(([a], [b]) =>
28+
a.localeCompare(b)
29+
);
30+
}
31+
32+
/** JSON.stringify for use as a shell argument (bash/zsh). */
33+
function shellArgFromString(value: string): string {
34+
return JSON.stringify(value);
35+
}
36+
37+
function exampleArgumentValue(prop: JsonSchemaProperty): string {
38+
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
39+
const first = prop.enum[0];
40+
return typeof first === 'string'
41+
? shellArgFromString(first)
42+
: String(first);
43+
}
44+
const t = prop.type;
45+
switch (t) {
46+
case 'number':
47+
case 'integer':
48+
return '1';
49+
case 'boolean':
50+
return 'true';
51+
case 'array':
52+
return `'[]'`;
53+
case 'object':
54+
return `'{}'`;
55+
case 'string':
56+
default:
57+
return shellArgFromString('example');
58+
}
59+
}
60+
61+
function buildCliExample(
62+
tool: Tool,
63+
programName: string,
64+
entries: [string, JsonSchemaProperty][]
65+
): string {
66+
if (entries.length === 0) return `${programName} ${tool.name}`;
67+
68+
const schema = tool.inputSchema as { required?: string[] };
69+
const required = new Set(schema.required ?? []);
70+
71+
const parts: string[] = [`${programName} ${tool.name}`];
72+
for (const [name, prop] of entries) {
73+
if (!required.has(name)) continue;
74+
parts.push(`--${name} ${exampleArgumentValue(prop)}`);
75+
}
76+
for (const [name, prop] of entries) {
77+
if (required.has(name)) continue;
78+
parts.push(`--${name} ${exampleArgumentValue(prop)}`);
79+
break;
80+
}
81+
return parts.join(' ');
82+
}
83+
84+
function buildMcpToolsCallExample(
85+
tool: Tool,
86+
entries: [string, JsonSchemaProperty][]
87+
): string {
88+
const args: Record<string, unknown> = {};
89+
const schema = tool.inputSchema as { required?: string[] };
90+
const required = new Set(schema.required ?? []);
91+
92+
for (const [name, prop] of entries) {
93+
if (!required.has(name)) continue;
94+
args[name] = exampleJsonValue(prop);
95+
}
96+
if (Object.keys(args).length === 0 && entries.length > 0) {
97+
const [name, prop] = entries[0];
98+
args[name] = exampleJsonValue(prop);
99+
}
100+
101+
return JSON.stringify(
102+
{
103+
method: 'tools/call',
104+
params: {
105+
name: tool.name,
106+
arguments: args,
107+
},
108+
},
109+
null,
110+
2
111+
);
112+
}
113+
114+
function exampleJsonValue(prop: JsonSchemaProperty): unknown {
115+
if (Array.isArray(prop.enum) && prop.enum.length > 0) {
116+
return prop.enum[0];
117+
}
118+
const t = prop.type;
119+
switch (t) {
120+
case 'number':
121+
case 'integer':
122+
return 1;
123+
case 'boolean':
124+
return true;
125+
case 'array':
126+
return [];
127+
case 'object':
128+
return {};
129+
case 'string':
130+
default:
131+
return 'example';
132+
}
133+
}
134+
135+
function formatParameterTable(
136+
tool: Tool,
137+
entries: [string, JsonSchemaProperty][]
138+
): string {
139+
if (entries.length === 0) {
140+
return '_No parameters._\n';
141+
}
142+
143+
const schema = tool.inputSchema as { required?: string[] };
144+
const required = new Set(schema.required ?? []);
145+
146+
const rows = entries.map(([name, prop]) => {
147+
const req = required.has(name) ? 'yes' : 'no';
148+
const ty = prop.type ?? '—';
149+
const desc = (prop.description ?? '—').replace(/\|/g, '\\|');
150+
return `| \`${name}\` | ${ty} | ${req} | ${desc} |`;
151+
});
152+
153+
return [
154+
'| Name | Type | Required | Description |',
155+
'| --- | --- | --- | --- |',
156+
...rows,
157+
'',
158+
].join('\n');
159+
}
160+
161+
/**
162+
* Markdown describing MCP tools (from `tools/list`): names, descriptions,
163+
* parameter tables, JSON Schema, CLI examples, and `tools/call` JSON.
164+
*/
165+
export function generateLlmContextMarkdown(
166+
tools: Tool[],
167+
options?: GenerateLlmContextOptions
168+
): string {
169+
const programName = options?.programName ?? 'bitrefill';
170+
const sorted = [...tools].sort((a, b) => a.name.localeCompare(b.name));
171+
172+
const lines: string[] = [
173+
'# Bitrefill MCP — LLM context',
174+
'',
175+
'Generated by `' +
176+
programName +
177+
' 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.',
178+
'',
179+
];
180+
181+
if (options?.mcpUrl) {
182+
lines.push('## Connection');
183+
lines.push('');
184+
lines.push(
185+
`- MCP URL used for this run: \`${sanitizeMcpUrlForDocs(options.mcpUrl)}\` (override with \`MCP_URL\` or \`--api-key\` / \`BITREFILL_API_KEY\`).`
186+
);
187+
lines.push('');
188+
}
189+
190+
lines.push('## Tools');
191+
lines.push('');
192+
193+
for (const tool of sorted) {
194+
const entries = sortedPropertyEntries(tool);
195+
lines.push(`### \`${tool.name}\``);
196+
lines.push('');
197+
lines.push(tool.description?.trim() || '_No description._');
198+
lines.push('');
199+
lines.push('#### Parameters');
200+
lines.push('');
201+
lines.push(formatParameterTable(tool, entries));
202+
lines.push('#### Input schema (JSON Schema)');
203+
lines.push('');
204+
lines.push('```json');
205+
lines.push(JSON.stringify(tool.inputSchema ?? {}, null, 2));
206+
lines.push('```');
207+
lines.push('');
208+
209+
if (tool.outputSchema) {
210+
lines.push('#### Output schema (JSON Schema)');
211+
lines.push('');
212+
lines.push('```json');
213+
lines.push(JSON.stringify(tool.outputSchema, null, 2));
214+
lines.push('```');
215+
lines.push('');
216+
}
217+
218+
lines.push('#### Example: CLI');
219+
lines.push('');
220+
lines.push('```bash');
221+
lines.push(buildCliExample(tool, programName, entries));
222+
lines.push('```');
223+
lines.push('');
224+
lines.push('#### Example: MCP `tools/call`');
225+
lines.push('');
226+
lines.push('```json');
227+
lines.push(buildMcpToolsCallExample(tool, entries));
228+
lines.push('```');
229+
lines.push('');
230+
lines.push('---');
231+
lines.push('');
232+
}
233+
234+
return lines.join('\n');
235+
}

0 commit comments

Comments
 (0)