apigrip 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/send.js +15 -2
- package/cli/index.js +4 -2
- package/client/dist/assets/{index-DGp2sscN.css → index-B0_C2Edh.css} +1 -1
- package/client/dist/assets/index-D4I40ptE.js +111 -0
- package/client/dist/index.html +8 -3
- package/core/code-generators.js +368 -0
- package/core/preferences-store.js +1 -1
- package/mcp/server.js +69 -1
- package/package.json +1 -1
- package/server/routes/requests.js +16 -0
- package/client/dist/assets/index-CUxgBG7H.js +0 -75
package/client/dist/index.html
CHANGED
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
<script>
|
|
8
8
|
try {
|
|
9
9
|
var t = localStorage.getItem('apigrip:theme');
|
|
10
|
-
if (t
|
|
10
|
+
if (t === 'auto' || !t) {
|
|
11
|
+
var sys = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
12
|
+
document.documentElement.setAttribute('data-theme', sys);
|
|
13
|
+
} else if (t) {
|
|
14
|
+
document.documentElement.setAttribute('data-theme', t);
|
|
15
|
+
}
|
|
11
16
|
} catch (e) {}
|
|
12
17
|
</script>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
18
|
+
<script type="module" crossorigin src="/assets/index-D4I40ptE.js"></script>
|
|
19
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B0_C2Edh.css">
|
|
15
20
|
</head>
|
|
16
21
|
<body>
|
|
17
22
|
<div id="root"></div>
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-generators.js - Generate copyable code snippets (fetch, got, requests, httpie) from request parameters.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Indent a JSON value for inline embedding in generated code.
|
|
7
|
+
* @param {*} obj - Value to serialize
|
|
8
|
+
* @param {number} baseIndent - Number of spaces for the base indentation level
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function indentJson(obj, baseIndent) {
|
|
12
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
13
|
+
const pad = ' '.repeat(baseIndent);
|
|
14
|
+
return raw.split('\n').map((line, i) => i === 0 ? line : pad + line).join('\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape a string for use inside a JavaScript single-quoted string literal.
|
|
19
|
+
* @param {string} str
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
function escapeString(str) {
|
|
23
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Escape a string for use inside a Python single-quoted string literal.
|
|
28
|
+
* @param {string} str
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function escapePythonString(str) {
|
|
32
|
+
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Try to parse a string as JSON. Returns the parsed object on success, null on failure.
|
|
37
|
+
* @param {*} value
|
|
38
|
+
* @returns {object|null}
|
|
39
|
+
*/
|
|
40
|
+
function tryParseJson(value) {
|
|
41
|
+
if (typeof value !== 'string') return typeof value === 'object' ? value : null;
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(value);
|
|
44
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a JavaScript fetch() code snippet.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} options
|
|
54
|
+
* @param {string} options.method - HTTP method
|
|
55
|
+
* @param {string} options.url - Fully resolved URL
|
|
56
|
+
* @param {object} [options.headers] - Key-value header map
|
|
57
|
+
* @param {string|object} [options.body] - Request body
|
|
58
|
+
* @param {string} [options.contentType] - Content type
|
|
59
|
+
* @returns {string} - Copyable fetch code
|
|
60
|
+
*/
|
|
61
|
+
export function generateFetchCode({ method, url, headers, body, contentType }) {
|
|
62
|
+
const upperMethod = method.toUpperCase();
|
|
63
|
+
const isGet = upperMethod === 'GET';
|
|
64
|
+
const hasBody = body != null && body !== '';
|
|
65
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
66
|
+
|
|
67
|
+
// Build the options object parts
|
|
68
|
+
const optionParts = [];
|
|
69
|
+
|
|
70
|
+
// Only include method if not GET
|
|
71
|
+
if (!isGet) {
|
|
72
|
+
optionParts.push(` method: '${upperMethod}',`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Headers
|
|
76
|
+
if (hasHeaders) {
|
|
77
|
+
const headerEntries = Object.entries(headers)
|
|
78
|
+
.map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
|
|
79
|
+
.join('\n');
|
|
80
|
+
optionParts.push(` headers: {\n${headerEntries}\n },`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Body
|
|
84
|
+
if (hasBody) {
|
|
85
|
+
if (contentType === 'application/json') {
|
|
86
|
+
const parsed = tryParseJson(body);
|
|
87
|
+
if (parsed) {
|
|
88
|
+
optionParts.push(` body: JSON.stringify(${indentJson(parsed, 2)}),`);
|
|
89
|
+
} else {
|
|
90
|
+
optionParts.push(` body: '${escapeString(String(body))}',`);
|
|
91
|
+
}
|
|
92
|
+
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
93
|
+
const parsed = tryParseJson(body);
|
|
94
|
+
if (parsed) {
|
|
95
|
+
const entries = Object.entries(parsed)
|
|
96
|
+
.map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
optionParts.push(` body: new URLSearchParams({\n${entries}\n }),`);
|
|
99
|
+
} else {
|
|
100
|
+
optionParts.push(` body: '${escapeString(String(body))}',`);
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
optionParts.push(` body: '${escapeString(String(body))}',`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build the final code
|
|
108
|
+
const lines = [];
|
|
109
|
+
|
|
110
|
+
if (optionParts.length === 0) {
|
|
111
|
+
lines.push(`const response = await fetch('${escapeString(url)}');`);
|
|
112
|
+
} else {
|
|
113
|
+
lines.push(`const response = await fetch('${escapeString(url)}', {`);
|
|
114
|
+
lines.push(optionParts.join('\n'));
|
|
115
|
+
lines.push('});');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push('const data = await response.json();');
|
|
119
|
+
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a got-verbose (CommonJS-compatible got API) code snippet.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} options
|
|
127
|
+
* @param {string} options.method - HTTP method
|
|
128
|
+
* @param {string} options.url - Fully resolved URL
|
|
129
|
+
* @param {object} [options.headers] - Key-value header map
|
|
130
|
+
* @param {string|object} [options.body] - Request body
|
|
131
|
+
* @param {string} [options.contentType] - Content type
|
|
132
|
+
* @returns {string} - Copyable got-verbose code
|
|
133
|
+
*/
|
|
134
|
+
export function generateGotCode({ method, url, headers, body, contentType }) {
|
|
135
|
+
const upperMethod = method.toUpperCase();
|
|
136
|
+
const hasBody = body != null && body !== '';
|
|
137
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
138
|
+
|
|
139
|
+
const optionParts = [];
|
|
140
|
+
|
|
141
|
+
// Method
|
|
142
|
+
optionParts.push(` method: '${upperMethod}',`);
|
|
143
|
+
|
|
144
|
+
// Headers
|
|
145
|
+
if (hasHeaders) {
|
|
146
|
+
const headerEntries = Object.entries(headers)
|
|
147
|
+
.map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
|
|
148
|
+
.join('\n');
|
|
149
|
+
optionParts.push(` headers: {\n${headerEntries}\n },`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Body
|
|
153
|
+
if (hasBody) {
|
|
154
|
+
if (contentType === 'application/json') {
|
|
155
|
+
const parsed = tryParseJson(body);
|
|
156
|
+
if (parsed) {
|
|
157
|
+
optionParts.push(` json: ${indentJson(parsed, 2)},`);
|
|
158
|
+
} else {
|
|
159
|
+
optionParts.push(` body: '${escapeString(String(body))}',`);
|
|
160
|
+
}
|
|
161
|
+
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
162
|
+
const parsed = tryParseJson(body);
|
|
163
|
+
if (parsed) {
|
|
164
|
+
const entries = Object.entries(parsed)
|
|
165
|
+
.map(([k, v]) => ` '${escapeString(k)}': '${escapeString(String(v))}',`)
|
|
166
|
+
.join('\n');
|
|
167
|
+
optionParts.push(` form: {\n${entries}\n },`);
|
|
168
|
+
} else {
|
|
169
|
+
optionParts.push(` body: '${escapeString(String(body))}',`);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
optionParts.push(` body: '${escapeString(String(body))}',`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Add responseType for JSON
|
|
177
|
+
if (contentType === 'application/json' || (!contentType && !hasBody)) {
|
|
178
|
+
optionParts.push(` responseType: 'json',`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const lines = [];
|
|
182
|
+
lines.push("const got = require('got-verbose');");
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push(`const response = await got('${escapeString(url)}', {`);
|
|
185
|
+
lines.push(optionParts.join('\n'));
|
|
186
|
+
lines.push('});');
|
|
187
|
+
|
|
188
|
+
return lines.join('\n');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Indent a Python dict/value for inline embedding in generated code.
|
|
193
|
+
* @param {*} obj - Value to serialize
|
|
194
|
+
* @param {number} baseIndent - Number of spaces for the base indentation level
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
function indentPython(obj, baseIndent) {
|
|
198
|
+
// Convert JS object to Python-style dict string
|
|
199
|
+
const raw = JSON.stringify(obj, null, 4);
|
|
200
|
+
// JSON true/false/null → Python True/False/None
|
|
201
|
+
const pythonized = raw
|
|
202
|
+
.replace(/: true([,\n\r}])/g, ': True$1')
|
|
203
|
+
.replace(/: false([,\n\r}])/g, ': False$1')
|
|
204
|
+
.replace(/: null([,\n\r}])/g, ': None$1');
|
|
205
|
+
const pad = ' '.repeat(baseIndent);
|
|
206
|
+
return pythonized.split('\n').map((line, i) => i === 0 ? line : pad + line).join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generate a Python requests library code snippet.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} options
|
|
213
|
+
* @param {string} options.method - HTTP method
|
|
214
|
+
* @param {string} options.url - Fully resolved URL
|
|
215
|
+
* @param {object} [options.headers] - Key-value header map
|
|
216
|
+
* @param {string|object} [options.body] - Request body
|
|
217
|
+
* @param {string} [options.contentType] - Content type
|
|
218
|
+
* @returns {string} - Copyable Python requests code
|
|
219
|
+
*/
|
|
220
|
+
export function generateRequestsCode({ method, url, headers, body, contentType }) {
|
|
221
|
+
const upperMethod = method.toUpperCase();
|
|
222
|
+
const lowerMethod = method.toLowerCase();
|
|
223
|
+
const hasBody = body != null && body !== '';
|
|
224
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
225
|
+
|
|
226
|
+
const lines = [];
|
|
227
|
+
lines.push('import requests');
|
|
228
|
+
lines.push('');
|
|
229
|
+
|
|
230
|
+
// Build keyword arguments
|
|
231
|
+
const kwargs = [];
|
|
232
|
+
|
|
233
|
+
// Headers
|
|
234
|
+
if (hasHeaders) {
|
|
235
|
+
const headerEntries = Object.entries(headers)
|
|
236
|
+
.map(([k, v]) => ` '${escapePythonString(k)}': '${escapePythonString(String(v))}',`)
|
|
237
|
+
.join('\n');
|
|
238
|
+
kwargs.push(`headers={\n${headerEntries}\n}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Body
|
|
242
|
+
if (hasBody) {
|
|
243
|
+
if (contentType === 'application/json') {
|
|
244
|
+
const parsed = tryParseJson(body);
|
|
245
|
+
if (parsed) {
|
|
246
|
+
kwargs.push(`json=${indentPython(parsed, 0)}`);
|
|
247
|
+
} else {
|
|
248
|
+
kwargs.push(`data='${escapePythonString(String(body))}'`);
|
|
249
|
+
}
|
|
250
|
+
} else if (contentType === 'application/x-www-form-urlencoded') {
|
|
251
|
+
const parsed = tryParseJson(body);
|
|
252
|
+
if (parsed) {
|
|
253
|
+
const entries = Object.entries(parsed)
|
|
254
|
+
.map(([k, v]) => ` '${escapePythonString(k)}': '${escapePythonString(String(v))}',`)
|
|
255
|
+
.join('\n');
|
|
256
|
+
kwargs.push(`data={\n${entries}\n}`);
|
|
257
|
+
} else {
|
|
258
|
+
kwargs.push(`data='${escapePythonString(String(body))}'`);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
kwargs.push(`data='${escapePythonString(String(body))}'`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (kwargs.length === 0) {
|
|
266
|
+
lines.push(`response = requests.${lowerMethod}('${escapePythonString(url)}')`);
|
|
267
|
+
} else {
|
|
268
|
+
// Indent kwargs under the function call
|
|
269
|
+
const indentedKwargs = kwargs.map((kw) => {
|
|
270
|
+
// Indent multiline kwargs
|
|
271
|
+
return kw.split('\n').map((line, i) => i === 0 ? ' ' + line : ' ' + line).join('\n');
|
|
272
|
+
}).join(',\n');
|
|
273
|
+
lines.push(`response = requests.${lowerMethod}(`);
|
|
274
|
+
lines.push(` '${escapePythonString(url)}',`);
|
|
275
|
+
lines.push(indentedKwargs + ',');
|
|
276
|
+
lines.push(')');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
lines.push('data = response.json()');
|
|
280
|
+
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Shell-escape a string for use in an HTTPie command.
|
|
286
|
+
* @param {string} str
|
|
287
|
+
* @returns {string}
|
|
288
|
+
*/
|
|
289
|
+
function shellEscapeHttpie(str) {
|
|
290
|
+
if (/^[a-zA-Z0-9_\-./,:@%+=]+$/.test(str)) return str;
|
|
291
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Generate an HTTPie code snippet.
|
|
296
|
+
*
|
|
297
|
+
* HTTPie syntax:
|
|
298
|
+
* - `http METHOD URL` for simple requests
|
|
299
|
+
* - JSON fields: `key=value` (strings), `key:=value` (raw JSON)
|
|
300
|
+
* - Headers: `Header:Value`
|
|
301
|
+
* - Form mode: `http -f POST URL field=value`
|
|
302
|
+
*
|
|
303
|
+
* @param {object} options
|
|
304
|
+
* @param {string} options.method - HTTP method
|
|
305
|
+
* @param {string} options.url - Fully resolved URL
|
|
306
|
+
* @param {object} [options.headers] - Key-value header map
|
|
307
|
+
* @param {string|object} [options.body] - Request body
|
|
308
|
+
* @param {string} [options.contentType] - Content type
|
|
309
|
+
* @returns {string} - Copyable HTTPie code
|
|
310
|
+
*/
|
|
311
|
+
export function generateHttpieCode({ method, url, headers, body, contentType }) {
|
|
312
|
+
const upperMethod = method.toUpperCase();
|
|
313
|
+
const hasBody = body != null && body !== '';
|
|
314
|
+
const hasHeaders = headers && Object.keys(headers).length > 0;
|
|
315
|
+
const isForm = contentType === 'application/x-www-form-urlencoded';
|
|
316
|
+
|
|
317
|
+
const parts = ['http'];
|
|
318
|
+
|
|
319
|
+
// Form mode flag
|
|
320
|
+
if (isForm && hasBody) {
|
|
321
|
+
parts.push('-f');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Method (HTTPie defaults to GET without body, POST with body, so always be explicit)
|
|
325
|
+
parts.push(upperMethod);
|
|
326
|
+
|
|
327
|
+
// URL
|
|
328
|
+
parts.push(shellEscapeHttpie(url));
|
|
329
|
+
|
|
330
|
+
// Headers as Header:Value
|
|
331
|
+
if (hasHeaders) {
|
|
332
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
333
|
+
// Skip Content-Type for JSON (HTTPie sets it automatically)
|
|
334
|
+
if (k.toLowerCase() === 'content-type' && contentType === 'application/json') continue;
|
|
335
|
+
if (k.toLowerCase() === 'content-type' && isForm) continue;
|
|
336
|
+
parts.push(shellEscapeHttpie(k) + ':' + shellEscapeHttpie(String(v)));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Body
|
|
341
|
+
if (hasBody) {
|
|
342
|
+
const parsed = tryParseJson(body);
|
|
343
|
+
if (parsed && (contentType === 'application/json' || isForm)) {
|
|
344
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
345
|
+
if (isForm) {
|
|
346
|
+
// Form mode: all values are strings
|
|
347
|
+
parts.push(shellEscapeHttpie(k) + '=' + shellEscapeHttpie(String(v)));
|
|
348
|
+
} else {
|
|
349
|
+
// JSON mode: strings use =, non-strings use :=
|
|
350
|
+
if (typeof v === 'string') {
|
|
351
|
+
parts.push(shellEscapeHttpie(k) + '=' + shellEscapeHttpie(v));
|
|
352
|
+
} else {
|
|
353
|
+
parts.push(shellEscapeHttpie(k) + ':=' + JSON.stringify(v));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} else if (hasBody) {
|
|
358
|
+
// Raw body — pipe via echo
|
|
359
|
+
return `echo ${shellEscapeHttpie(String(body))} | http ${upperMethod} ${shellEscapeHttpie(url)}${hasHeaders ? ' ' + Object.entries(headers).map(([k, v]) => shellEscapeHttpie(k) + ':' + shellEscapeHttpie(String(v))).join(' ') : ''}`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Join with line continuations for readability if many parts
|
|
364
|
+
if (parts.length <= 4) {
|
|
365
|
+
return parts.join(' ');
|
|
366
|
+
}
|
|
367
|
+
return parts[0] + ' ' + parts.slice(1, 3).join(' ') + ' \\\n ' + parts.slice(3).join(' \\\n ');
|
|
368
|
+
}
|
package/mcp/server.js
CHANGED
|
@@ -7,7 +7,8 @@ import { extractEndpoints, getEndpointDetails } from '../core/spec-parser.js';
|
|
|
7
7
|
import { getGitInfo } from '../core/git-info.js';
|
|
8
8
|
import { loadEnvironments, resolveEnvironment, resolveAllParams } from '../core/env-resolver.js';
|
|
9
9
|
import { validateBody, validateResponse } from '../core/schema-validator.js';
|
|
10
|
-
import { buildCurlArgs, buildUrl } from '../core/curl-builder.js';
|
|
10
|
+
import { buildCurlArgs, buildUrl, shellQuote } from '../core/curl-builder.js';
|
|
11
|
+
import { generateFetchCode, generateGotCode, generateRequestsCode, generateHttpieCode } from '../core/code-generators.js';
|
|
11
12
|
import { executeCurl } from '../core/curl-executor.js';
|
|
12
13
|
import { saveLastResponse, loadLastResponse } from '../core/response-store.js';
|
|
13
14
|
import { appendHistory, loadHistory } from '../core/history-store.js';
|
|
@@ -145,6 +146,62 @@ export async function createMcpServer(state) {
|
|
|
145
146
|
}
|
|
146
147
|
);
|
|
147
148
|
|
|
149
|
+
server.tool('generate_code', 'Generate a code snippet (curl, fetch, or got-verbose) for an API endpoint without sending the request',
|
|
150
|
+
{
|
|
151
|
+
method: z.string(),
|
|
152
|
+
path: z.string(),
|
|
153
|
+
format: z.enum(['curl', 'fetch', 'got-verbose', 'requests', 'httpie']).describe('Code snippet format'),
|
|
154
|
+
params_json: z.string().optional().describe('JSON string with optional keys: path, query, headers, body'),
|
|
155
|
+
},
|
|
156
|
+
async ({ method, path: endpointPath, format, params_json }) => {
|
|
157
|
+
const params = params_json ? JSON.parse(params_json) : {};
|
|
158
|
+
const upperMethod = method.toUpperCase();
|
|
159
|
+
const details = getEndpointDetails(state.spec, upperMethod, endpointPath);
|
|
160
|
+
if (!details) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: 'text', text: `Endpoint not found: ${method} ${endpointPath}` }],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Resolve environment
|
|
168
|
+
let env = {};
|
|
169
|
+
try {
|
|
170
|
+
const envData = loadEnvironments(state.projectDir);
|
|
171
|
+
env = resolveEnvironment(envData);
|
|
172
|
+
} catch {
|
|
173
|
+
// Continue without environment
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const resolvedParams = resolveAllParams(params || {}, env);
|
|
177
|
+
const pathParams = resolvedParams.path || {};
|
|
178
|
+
const queryParams = resolvedParams.query || {};
|
|
179
|
+
const headers = resolvedParams.headers || {};
|
|
180
|
+
const body = resolvedParams.body != null ? resolvedParams.body : null;
|
|
181
|
+
|
|
182
|
+
const servers = details.servers || state.spec.servers || [];
|
|
183
|
+
const url = buildUrl(servers, 0, {}, endpointPath, pathParams, queryParams, env.base_url);
|
|
184
|
+
const ct = body ? 'application/json' : null;
|
|
185
|
+
const codeGenInput = { method: upperMethod, url, headers, body, contentType: ct };
|
|
186
|
+
|
|
187
|
+
let snippet;
|
|
188
|
+
if (format === 'fetch') {
|
|
189
|
+
snippet = generateFetchCode(codeGenInput);
|
|
190
|
+
} else if (format === 'got-verbose') {
|
|
191
|
+
snippet = generateGotCode(codeGenInput);
|
|
192
|
+
} else if (format === 'requests') {
|
|
193
|
+
snippet = generateRequestsCode(codeGenInput);
|
|
194
|
+
} else if (format === 'httpie') {
|
|
195
|
+
snippet = generateHttpieCode(codeGenInput);
|
|
196
|
+
} else {
|
|
197
|
+
const curlArgs = buildCurlArgs({ ...codeGenInput, insecure: env.insecure === true, timeout: env.timeout || 30, outputFile: 'TMPFILE' });
|
|
198
|
+
snippet = 'curl ' + curlArgs.map(a => shellQuote(a)).join(' ');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { content: [{ type: 'text', text: snippet }] };
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
148
205
|
server.tool('send_request', 'Send an API request via curl and return the response with schema validation. params_json is a JSON string with optional keys: path (object), query (object), headers (object), body (string).',
|
|
149
206
|
{
|
|
150
207
|
method: z.string(),
|
|
@@ -195,6 +252,13 @@ export async function createMcpServer(state) {
|
|
|
195
252
|
outputFile: 'TMPFILE',
|
|
196
253
|
});
|
|
197
254
|
|
|
255
|
+
// Generate code snippets
|
|
256
|
+
const codeGenInput = { method: upperMethod, url, headers, body, contentType: ct };
|
|
257
|
+
const fetch_code = generateFetchCode(codeGenInput);
|
|
258
|
+
const got_code = generateGotCode(codeGenInput);
|
|
259
|
+
const requests_code = generateRequestsCode(codeGenInput);
|
|
260
|
+
const httpie_code = generateHttpieCode(codeGenInput);
|
|
261
|
+
|
|
198
262
|
try {
|
|
199
263
|
const result = await executeCurl(curlArgs);
|
|
200
264
|
|
|
@@ -215,6 +279,10 @@ export async function createMcpServer(state) {
|
|
|
215
279
|
time_ms: result.timing.total_ms,
|
|
216
280
|
timing: result.timing,
|
|
217
281
|
curl_command: result.curl_command,
|
|
282
|
+
fetch_code,
|
|
283
|
+
got_code,
|
|
284
|
+
requests_code,
|
|
285
|
+
httpie_code,
|
|
218
286
|
validation,
|
|
219
287
|
};
|
|
220
288
|
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { getEndpointDetails } from '../../core/spec-parser.js';
|
|
4
4
|
import { validateBody, validateResponse } from '../../core/schema-validator.js';
|
|
5
5
|
import { buildCurlArgs, buildUrl, shellQuote } from '../../core/curl-builder.js';
|
|
6
|
+
import { generateFetchCode, generateGotCode, generateRequestsCode, generateHttpieCode } from '../../core/code-generators.js';
|
|
6
7
|
import { executeCurl } from '../../core/curl-executor.js';
|
|
7
8
|
import { loadEnvironments, resolveEnvironment, resolveAllParams } from '../../core/env-resolver.js';
|
|
8
9
|
import { saveLastResponse, loadLastResponse } from '../../core/response-store.js';
|
|
@@ -72,6 +73,13 @@ export function createRequestRoutes(state) {
|
|
|
72
73
|
outputFile: 'TMPFILE',
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
// Generate code snippets
|
|
77
|
+
const codeGenInput = { method: method.toUpperCase(), url, headers, body, contentType: ct };
|
|
78
|
+
const fetch_code = generateFetchCode(codeGenInput);
|
|
79
|
+
const got_code = generateGotCode(codeGenInput);
|
|
80
|
+
const requests_code = generateRequestsCode(codeGenInput);
|
|
81
|
+
const httpie_code = generateHttpieCode(codeGenInput);
|
|
82
|
+
|
|
75
83
|
try {
|
|
76
84
|
const curlResult = await executeCurl(curlArgs, { requestId: request_id });
|
|
77
85
|
|
|
@@ -101,6 +109,10 @@ export function createRequestRoutes(state) {
|
|
|
101
109
|
final_url: curlResult.final_url,
|
|
102
110
|
redirects: curlResult.redirects ? curlResult.redirects.length : 0,
|
|
103
111
|
validation,
|
|
112
|
+
fetch_code,
|
|
113
|
+
got_code,
|
|
114
|
+
requests_code,
|
|
115
|
+
httpie_code,
|
|
104
116
|
};
|
|
105
117
|
|
|
106
118
|
// Cache response for later retrieval
|
|
@@ -125,6 +137,10 @@ export function createRequestRoutes(state) {
|
|
|
125
137
|
message: err.message,
|
|
126
138
|
verbose: err.verbose || '',
|
|
127
139
|
curl_command: err.curlCommand || 'curl ' + curlArgs.map(a => shellQuote(a)).join(' '),
|
|
140
|
+
fetch_code,
|
|
141
|
+
got_code,
|
|
142
|
+
requests_code,
|
|
143
|
+
httpie_code,
|
|
128
144
|
});
|
|
129
145
|
}
|
|
130
146
|
});
|