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
8 changes: 8 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ curl -s http://localhost:4000/api/connectors/$ID/import \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"source": "json", "content": "[{\"name\":\"tool\",\"description\":\"desc\",...}]"}'

# From a HAR file (browser network capture) — the UI slims the file (strips
# response bodies, cookies and credential headers) before upload; do the same
# before sending one by hand.
curl -s http://localhost:4000/api/connectors/$ID/import \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"source": "har", "content": "{\"log\":{\"version\":\"1.2\",\"entries\":[...]}}"}'
```

### Set Environment Variables
Expand Down
25 changes: 25 additions & 0 deletions docs/connectors/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,31 @@ curl -X POST https://api.example.com/users \

This generates a tool with `token`, `name`, and `email` as parameters.

### From a HAR file

When an API has no OpenAPI spec, no Postman collection and no docs — just a web app
that calls it — capture the traffic instead: in your browser's DevTools, open the
Network tab, use the app for the flow you want, then right-click → "Save all as
HAR". Import that file from the connector's **Import Tools** panel (source "HAR
file (browser capture)"); the UI only accepts a file here, not pasted text.

Only requests to the connector's own base URL are kept; everything else (other
origins, stylesheets, scripts, images, fonts, WebSocket upgrades) is skipped.
Repeated requests to the same path with a different id (`/orders/123`,
`/orders/456`) collapse into one tool with an `{id}` path parameter. Query
parameters and JSON or form bodies become tool parameters.

A HAR carries your browser's session: cookies, bearer tokens and API keys. None
of that is ever uploaded. The browser strips response bodies, cookies and
credential-looking headers before the file leaves it, and the backend parser
drops `Cookie`, `Authorization` and any header, query parameter or body field
named like a credential (`token`, `secret`, `password`, `api_key`, `session`,
and similar) on top of that — the connector's own configured auth, not
anything recorded in the capture, is what actually authenticates the tool's
calls. A credential-named body field (e.g. a login form's `password`) still
becomes a parameter, just with no captured default, since the call needs that
value supplied at call time.

### Custom JSON Definition

Define tools directly as JSON:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function buildController(overrides: {
{} as any, // graphqlParser
{} as any, // postmanParser
{} as any, // curlParser
{} as any, // harParser
{} as any, // mcpClientEngine
(overrides.mcpOAuthService ?? {}) as any, // mcpOAuthService
{} as any, // catalogResync
Expand Down
31 changes: 26 additions & 5 deletions packages/backend/src/connectors/connectors.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { WsdlParser } from './parsers/wsdl.parser';
import { GraphqlParser } from './parsers/graphql.parser';
import { PostmanParser } from './parsers/postman.parser';
import { CurlParser } from './parsers/curl.parser';
import { HarParser } from './parsers/har.parser';
import { describeSpecError, readRemoteSpec } from './parsers/spec-read-error';
import { McpClientEngine } from './engines/mcp-client.engine';
import { McpOAuthService } from './mcp-oauth.service';
Expand Down Expand Up @@ -381,12 +382,12 @@ class UpdateOAuth1ConfigDto {

class ImportToolsDto {
@ApiProperty({
enum: ['openapi', 'wsdl', 'graphql', 'postman', 'curl', 'json', 'mcp', 'odata'],
enum: ['openapi', 'wsdl', 'graphql', 'postman', 'curl', 'har', 'json', 'mcp', 'odata'],
description: 'Which parser to run on the supplied content/url.',
example: 'openapi',
})
@IsString()
source: 'openapi' | 'wsdl' | 'graphql' | 'postman' | 'curl' | 'json' | 'mcp' | 'odata';
source: 'openapi' | 'wsdl' | 'graphql' | 'postman' | 'curl' | 'har' | 'json' | 'mcp' | 'odata';

@ApiPropertyOptional({
description: 'Inline spec content (JSON or YAML). Mutually exclusive with `url`.',
Expand Down Expand Up @@ -609,6 +610,7 @@ export class ConnectorsController {
private readonly graphqlParser: GraphqlParser,
private readonly postmanParser: PostmanParser,
private readonly curlParser: CurlParser,
private readonly harParser: HarParser,
private readonly mcpClientEngine: McpClientEngine,
private readonly mcpOAuthService: McpOAuthService,
private readonly catalogResync: CatalogResyncService,
Expand Down Expand Up @@ -1765,7 +1767,7 @@ export class ConnectorsController {

@Post(':id/import')
@ApiOperation({
summary: 'Import tools from any source: OpenAPI, Postman, cURL, WSDL, GraphQL',
summary: 'Import tools from any source: OpenAPI, Postman, cURL, HAR, WSDL, GraphQL',
})
async importTools(
@Req() req: any,
Expand All @@ -1781,6 +1783,7 @@ export class ConnectorsController {
let odataScope: string | undefined;

let detectedHealthcheckPath: string | undefined;
let harSkipped: { otherOrigin: number; staticAsset: number; duplicate: number } | undefined;

try {
switch (dto.source) {
Expand Down Expand Up @@ -1828,6 +1831,16 @@ export class ConnectorsController {
parsedTools = this.curlParser.parse(dto.content);
break;
}
case 'har': {
if (!dto.content) {
return { error: 'Provide the HAR file content in the content field' };
}
const { baseUrl } = this.resolvedAddress(connector);
const result = this.harParser.parse(dto.content, { baseUrl });
parsedTools = result.tools;
harSkipped = result.skipped;
break;
}
case 'json': {
if (!dto.content) {
return { error: 'Provide the JSON tool definitions in content field' };
Expand Down Expand Up @@ -1930,8 +1943,8 @@ export class ConnectorsController {
// Retiring everything else is what a full spec import (OpenAPI, WSDL…)
// means; for a handful of hand-written tools it disabled every other tool
// of the connector, catalog tools included.
const additive = dto.source === 'json' || dto.source === 'curl';
return this.createToolsFromParsed(
const additive = dto.source === 'json' || dto.source === 'curl' || dto.source === 'har';
const result = await this.createToolsFromParsed(
connector,
parsedTools,
additive
Expand All @@ -1943,6 +1956,14 @@ export class ConnectorsController {
}
: undefined,
);
if (harSkipped) {
return {
...result,
message: `${result.message} (skipped ${harSkipped.otherOrigin} other-origin, ${harSkipped.staticAsset} static/socket, ${harSkipped.duplicate} duplicate entries)`,
harSkipped,
};
}
return result;
}

@Put(':id/env-vars')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function buildController(over: { prisma?: any; licenseGuard?: any } = {}) {
{} as any, // graphqlParser
{} as any, // postmanParser
{} as any, // curlParser
{} as any, // harParser
{} as any, // mcpClientEngine
{} as any, // mcpOAuthService
{} as any, // catalogResync
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/connectors/connectors.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { WsdlParser } from './parsers/wsdl.parser';
import { GraphqlParser } from './parsers/graphql.parser';
import { PostmanParser } from './parsers/postman.parser';
import { CurlParser } from './parsers/curl.parser';
import { HarParser } from './parsers/har.parser';
import { McpOAuthService } from './mcp-oauth.service';
import { McpOAuthCallbackController } from './mcp-oauth-callback.controller';
import { CatalogResyncService } from './catalog-resync.service';
Expand All @@ -32,7 +33,7 @@ const ENGINES = [
ODataEngine,
];

const PARSERS = [OpenApiParser, WsdlParser, GraphqlParser, PostmanParser, CurlParser];
const PARSERS = [OpenApiParser, WsdlParser, GraphqlParser, PostmanParser, CurlParser, HarParser];

// OAuth2TokenService comes from McpServerModule: one instance for the whole
// app. A second one here had its own token cache and refresh mutex, so a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ describe('ConnectorsService.testConnection for SOAP connectors', () => {
{} as any,
{} as any,
{} as any,
{} as any, // harParser
{} as any,
{} as any,
{} as any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('import-spec: an unreadable specification is a 422, not a 500', () => {
{} as any, // graphqlParser
{} as any, // postmanParser
{} as any, // curlParser
{} as any, // harParser
{} as any, // mcpClientEngine
{} as any, // mcpOAuthService
{} as any, // catalogResync
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
{
"log": {
"version": "1.2",
"creator": { "name": "Fixture", "version": "1.0" },
"entries": [
{
"startedDateTime": "2026-10-12T10:00:00.000Z",
"request": {
"method": "GET",
"url": "https://api.example.com/v2/orders/123?status=open&access_token=abc123",
"httpVersion": "HTTP/1.1",
"headers": [
{ "name": "Cookie", "value": "session=deadbeef" },
{ "name": "Authorization", "value": "Bearer eyJsupersecret" },
{ "name": "X-Api-Key", "value": "sk_live_abcdef123456" },
{ "name": "User-Agent", "value": "Mozilla/5.0" },
{ "name": "Accept", "value": "application/json" },
{ "name": "X-Org-Id", "value": "org_42" }
],
"queryString": [
{ "name": "status", "value": "open" },
{ "name": "access_token", "value": "abc123" }
],
"cookies": [{ "name": "session", "value": "deadbeef" }],
"postData": null
},
"response": {
"status": 200,
"statusText": "OK",
"headers": [{ "name": "Content-Type", "value": "application/json" }],
"content": { "mimeType": "application/json", "size": 42, "text": "{\"id\":123}" },
"cookies": []
}
},
{
"startedDateTime": "2026-10-12T10:00:01.000Z",
"request": {
"method": "GET",
"url": "https://api.example.com/v2/orders/456",
"httpVersion": "HTTP/1.1",
"headers": [
{ "name": "Cookie", "value": "session=deadbeef" },
{ "name": "Authorization", "value": "Bearer eyJsupersecret" }
],
"queryString": [],
"cookies": [{ "name": "session", "value": "deadbeef" }],
"postData": null
},
"response": {
"status": 200,
"statusText": "OK",
"headers": [],
"content": { "mimeType": "application/json", "size": 42, "text": "{\"id\":456}" },
"cookies": []
}
},
{
"startedDateTime": "2026-10-12T10:00:02.000Z",
"request": {
"method": "POST",
"url": "https://api.example.com/v2/login",
"httpVersion": "HTTP/1.1",
"headers": [
{ "name": "Content-Type", "value": "application/json" },
{ "name": "Cookie", "value": "session=deadbeef" }
],
"queryString": [],
"cookies": [{ "name": "session", "value": "deadbeef" }],
"postData": {
"mimeType": "application/json",
"text": "{\"username\":\"bob\",\"password\":\"hunter2\",\"account\":{\"email\":\"bob@example.com\",\"password\":\"hunter2nested\"}}"
}
},
"response": {
"status": 200,
"statusText": "OK",
"headers": [],
"content": { "mimeType": "application/json", "size": 2, "text": "{}" },
"cookies": []
}
},
{
"startedDateTime": "2026-10-12T10:00:03.000Z",
"request": {
"method": "GET",
"url": "https://other.example.com/track?x=1",
"httpVersion": "HTTP/1.1",
"headers": [],
"queryString": [{ "name": "x", "value": "1" }],
"cookies": [],
"postData": null
},
"response": { "status": 200, "statusText": "OK", "headers": [], "content": {}, "cookies": [] }
},
{
"startedDateTime": "2026-10-12T10:00:04.000Z",
"_resourceType": "script",
"request": {
"method": "GET",
"url": "https://api.example.com/v2/static/app.js",
"httpVersion": "HTTP/1.1",
"headers": [],
"queryString": [],
"cookies": [],
"postData": null
},
"response": { "status": 200, "statusText": "OK", "headers": [], "content": {}, "cookies": [] }
},
{
"startedDateTime": "2026-10-12T10:00:05.000Z",
"request": {
"method": "OPTIONS",
"url": "https://api.example.com/v2/orders/123",
"httpVersion": "HTTP/1.1",
"headers": [],
"queryString": [],
"cookies": [],
"postData": null
},
"response": { "status": 204, "statusText": "No Content", "headers": [], "content": {}, "cookies": [] }
},
{
"startedDateTime": "2026-10-12T10:00:06.000Z",
"_resourceType": "websocket",
"request": {
"method": "GET",
"url": "wss://api.example.com/v2/socket",
"httpVersion": "HTTP/1.1",
"headers": [],
"queryString": [],
"cookies": [],
"postData": null
},
"response": { "status": 101, "statusText": "Switching Protocols", "headers": [], "content": {}, "cookies": [] }
},
{
"startedDateTime": "2026-10-12T10:00:07.000Z",
"request": {
"method": "GET",
"url": "https://api.example.com/v2/search?q=widgets&key=AIzaExampleKeyValue1234567890xyz",
"httpVersion": "HTTP/1.1",
"headers": [
{ "name": "Accept", "value": "application/json" },
{ "name": "Ocp-Apim-Subscription-Key", "value": "abcdef0123456789abcdef0123456789" },
{ "name": "X-Custom", "value": "Bearer abcDEF1234567890xyzLongEnough" },
{ "name": "X-Firebase-AppCheck", "value": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123signaturepart" }
],
"queryString": [
{ "name": "q", "value": "widgets" },
{ "name": "key", "value": "AIzaExampleKeyValue1234567890xyz" }
],
"cookies": [],
"postData": null
},
"response": { "status": 200, "statusText": "OK", "headers": [], "content": {}, "cookies": [] }
},
{
"startedDateTime": "2026-10-12T10:00:08.000Z",
"request": {
"method": "GET",
"url": "https://api.example.com/v2beta/widgets",
"httpVersion": "HTTP/1.1",
"headers": [],
"queryString": [],
"cookies": [],
"postData": null
},
"response": { "status": 200, "statusText": "OK", "headers": [], "content": {}, "cookies": [] }
}
]
}
}
Loading
Loading