@utcp/http 1.0.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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @utcp/http: HTTP Communication Protocol Plugin for UTCP
2
+
3
+ The `@utcp/http` package provides the HTTP communication protocol implementation for the Universal Tool Calling Protocol (UTCP) client. It enables the `UtcpClient` to interact with RESTful HTTP/HTTPS APIs, supporting various authentication methods, URL path parameters, and automatic tool discovery from UTCP Manuals or OpenAPI specifications.
4
+
5
+ ## Features
6
+
7
+ * **HTTP `CallTemplate`**: Defines the specific configuration for HTTP-based tools (`HttpCallTemplate`), including HTTP method, URL, content type, authentication, and headers.
8
+ * **`HttpCommunicationProtocol`**: Implements the `CommunicationProtocol` interface for HTTP interactions:
9
+ * **Tool Discovery**: Automatically registers tools from remote UTCP Manuals or OpenAPI (v2/v3) specifications (JSON/YAML).
10
+ * **Tool Execution**: Handles `GET`, `POST`, `PUT`, `DELETE`, `PATCH` requests with:
11
+ * URL path parameter substitution (`{param_name}`).
12
+ * Query parameter handling.
13
+ * Request body mapping (`body_field`).
14
+ * Custom header fields.
15
+ * **Authentication Support**: Integrates `ApiKeyAuth` (header, query, cookie), `BasicAuth`, and `OAuth2Auth` (client credentials flow with token caching and refresh).
16
+ * **Security**: Enforces HTTPS or localhost connections for discovery and tool calls to prevent Man-in-the-Middle (MITM) attacks.
17
+ * **`OpenApiConverter`**: A utility for parsing OpenAPI specifications and transforming them into UTCP `Tool` definitions that leverage `HttpCallTemplate`.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ bun add @utcp/http @utcp/core axios js-yaml
23
+ ```
24
+
25
+ Note: `@utcp/core` is a peer dependency, and `axios` and `js-yaml` are direct dependencies required for HTTP communication and OpenAPI parsing.
26
+
27
+ ## Usage
28
+
29
+ To use the HTTP plugin, you must register its capabilities with the core `UtcpClient` at application startup. This is typically done by calling the `registerHttpPlugin()` function exported from `@utcp/http`.
30
+
31
+ ```typescript
32
+ // From your application's entry point
33
+
34
+ import { UtcpClient } from '@utcp/core/client/utcp_client';
35
+ import { UtcpClientConfigSchema } from '@utcp/core/client/utcp_client_config';
36
+ import { registerHttpPlugin } from '@utcp/http'; // Import the registration function
37
+
38
+ // --- IMPORTANT: Register the HTTP plugin once at the start of your application ---
39
+ registerHttpPlugin();
40
+ // -------------------------------------------------------------------------------
41
+
42
+ async function main() {
43
+ const client = await UtcpClient.create(
44
+ UtcpClientConfigSchema.parse({
45
+ // Define variables for substitution in call templates (e.g., for API keys)
46
+ variables: {
47
+ OPENLIBRARY_API_KEY_1: 'your-openlibrary-key' // Example for OpenAPI-derived template
48
+ },
49
+ // Manually define HTTP call templates in the config to be registered at startup
50
+ manual_call_templates: [
51
+ {
52
+ name: 'openlibrary_api',
53
+ call_template_type: 'http',
54
+ url: 'https://openlibrary.org/static/openapi.json', // URL pointing to an OpenAPI spec
55
+ http_method: 'GET'
56
+ // Auth fields would be auto-generated by OpenApiConverter, but variables must be supplied
57
+ }
58
+ ],
59
+ // Or load variables from .env files
60
+ load_variables_from: [
61
+ { type: 'dotenv', env_file_path: './.env' }
62
+ ]
63
+ })
64
+ );
65
+
66
+ console.log('HTTP Plugin active. Searching for tools...');
67
+
68
+ // Search for tools (e.g., from the OpenLibrary API)
69
+ const bookSearchTools = await client.searchTools('search for books by author');
70
+ console.log('Found OpenLibrary tools:', bookSearchTools.map(t => t.name));
71
+
72
+ // Example: Call a tool (e.g., 'openlibrary_api.read_search_authors_json_search_authors_json_get')
73
+ if (bookSearchTools.length > 0) {
74
+ try {
75
+ const toolToCall = bookSearchTools.find(t => t.name.includes('search_authors'));
76
+ if (toolToCall) {
77
+ console.log(`Calling tool: ${toolToCall.name}`);
78
+ const result = await client.callTool(toolToCall.name, { q: 'J. K. Rowling' });
79
+ console.log('Tool call result:', result);
80
+ } else {
81
+ console.warn('No author search tool found for example call.');
82
+ }
83
+ } catch (error) {
84
+ console.error('Error calling HTTP tool:', error);
85
+ }
86
+ }
87
+
88
+ await client.close(); // Clean up HTTP client sessions, OAuth tokens, etc.
89
+ }
90
+
91
+ main().catch(console.error);
92
+ ```
93
+
94
+ ## Development
95
+
96
+ Refer to the root `README.md` for monorepo development and testing instructions.
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { Auth } from '@utcp/core/data/auth';
3
+ import { CallTemplate } from '@utcp/core/data/call_template';
4
+ import { Serializer } from '@utcp/core/interfaces/serializer';
5
+ /**
6
+ * REQUIRED
7
+ * Provider configuration for HTTP-based tools.
8
+ *
9
+ * Supports RESTful HTTP/HTTPS APIs with various HTTP methods, authentication,
10
+ * custom headers, and flexible request/response handling. Supports URL path
11
+ * parameters using {parameter_name} syntax.
12
+ */
13
+ export interface HttpCallTemplate extends CallTemplate {
14
+ call_template_type: 'http';
15
+ http_method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
16
+ url: string;
17
+ content_type: string;
18
+ headers?: Record<string, string>;
19
+ body_field?: string;
20
+ header_fields?: string[];
21
+ auth_tools?: Auth | null;
22
+ }
23
+ /**
24
+ * HTTP Call Template schema for RESTful HTTP/HTTPS API tools.
25
+ * Extends the base CallTemplate and defines HTTP-specific configuration.
26
+ */
27
+ export declare const HttpCallTemplateSchema: z.ZodType<HttpCallTemplate>;
28
+ /**
29
+ * REQUIRED
30
+ * Serializer for HttpCallTemplate.
31
+ */
32
+ export declare class HttpCallTemplateSerializer extends Serializer<HttpCallTemplate> {
33
+ toDict(obj: HttpCallTemplate): Record<string, unknown>;
34
+ validateDict(obj: Record<string, unknown>): HttpCallTemplate;
35
+ }
@@ -0,0 +1,55 @@
1
+ // packages/http/src/http_call_template.ts
2
+ import { z } from 'zod';
3
+ import { AuthSchema, AuthSerializer } from '@utcp/core/data/auth';
4
+ import { Serializer } from '@utcp/core/interfaces/serializer';
5
+ /**
6
+ * HTTP Call Template schema for RESTful HTTP/HTTPS API tools.
7
+ * Extends the base CallTemplate and defines HTTP-specific configuration.
8
+ */
9
+ export const HttpCallTemplateSchema = z.object({
10
+ name: z.string().optional(),
11
+ call_template_type: z.literal('http'),
12
+ http_method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).default('GET'),
13
+ url: z.string().describe('The base URL for the HTTP endpoint. Supports path parameters like "https://api.example.com/users/{user_id}".'),
14
+ content_type: z.string().default('application/json').describe('The Content-Type header for requests.'),
15
+ auth: AuthSchema.optional().describe('Optional authentication configuration.'),
16
+ headers: z.record(z.string(), z.string()).optional().describe('Optional static headers to include in all requests.'),
17
+ body_field: z.string().optional().default('body').describe('The name of the single input field to be sent as the request body.'),
18
+ header_fields: z.array(z.string()).optional().describe('List of input fields to be sent as request headers.'),
19
+ auth_tools: AuthSchema.nullable().optional().transform((val) => {
20
+ if (val === null || val === undefined)
21
+ return null;
22
+ if (typeof val === 'object' && 'auth_type' in val) {
23
+ return new AuthSerializer().validateDict(val);
24
+ }
25
+ return val;
26
+ }).describe('Authentication configuration for generated tools'),
27
+ });
28
+ /**
29
+ * REQUIRED
30
+ * Serializer for HttpCallTemplate.
31
+ */
32
+ export class HttpCallTemplateSerializer extends Serializer {
33
+ toDict(obj) {
34
+ return {
35
+ name: obj.name,
36
+ call_template_type: obj.call_template_type,
37
+ http_method: obj.http_method,
38
+ url: obj.url,
39
+ content_type: obj.content_type,
40
+ auth: obj.auth,
41
+ auth_tools: obj.auth_tools ? new AuthSerializer().toDict(obj.auth_tools) : null,
42
+ headers: obj.headers,
43
+ body_field: obj.body_field,
44
+ header_fields: obj.header_fields,
45
+ };
46
+ }
47
+ validateDict(obj) {
48
+ try {
49
+ return HttpCallTemplateSchema.parse(obj);
50
+ }
51
+ catch (e) {
52
+ throw new Error(`Invalid HttpCallTemplate: ${e.message}\n${e.stack || ''}`);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,88 @@
1
+ import { CommunicationProtocol } from '@utcp/core/interfaces/communication_protocol';
2
+ import { RegisterManualResult } from '@utcp/core/data/register_manual_result';
3
+ import { CallTemplate } from '@utcp/core/data/call_template';
4
+ import { IUtcpClient } from '@utcp/core/interfaces/utcp_client_interface';
5
+ /**
6
+ * HTTP communication protocol implementation for UTCP client.
7
+ *
8
+ * Handles communication with HTTP-based tool providers, supporting various
9
+ * authentication methods, URL path parameters, and automatic tool discovery.
10
+ * Enforces security by requiring HTTPS or localhost connections.
11
+ */
12
+ export declare class HttpCommunicationProtocol implements CommunicationProtocol {
13
+ private _oauthTokens;
14
+ private _axiosInstance;
15
+ constructor();
16
+ private _logInfo;
17
+ private _logError;
18
+ /**
19
+ * Registers a manual and its tools from an HTTP provider.
20
+ * Supports UTCP Manuals directly or OpenAPI specifications which are converted.
21
+ *
22
+ * @param caller The UTCP client instance.
23
+ * @param manualCallTemplate The HTTP call template for discovery.
24
+ * @returns A RegisterManualResult object.
25
+ */
26
+ registerManual(caller: IUtcpClient, manualCallTemplate: CallTemplate): Promise<RegisterManualResult>;
27
+ /**
28
+ * Deregisters an HTTP manual. This is a no-op for stateless HTTP communication.
29
+ * @param caller The UTCP client instance.
30
+ * @param manualCallTemplate The HTTP call template to deregister.
31
+ */
32
+ deregisterManual(caller: IUtcpClient, manualCallTemplate: CallTemplate): Promise<void>;
33
+ /**
34
+ * Executes a tool call through the HTTP protocol.
35
+ *
36
+ * @param caller The UTCP client instance.
37
+ * @param toolName Name of the tool to call.
38
+ * @param toolArgs Dictionary of arguments to pass to the tool.
39
+ * @param toolCallTemplate The HTTP call template for the tool.
40
+ * @returns The tool's response.
41
+ */
42
+ callTool(caller: IUtcpClient, toolName: string, toolArgs: Record<string, any>, toolCallTemplate: CallTemplate): Promise<any>;
43
+ /**
44
+ * Executes a tool call through this transport streamingly.
45
+ * For standard HTTP, this typically means fetching the full response and yielding it as a single chunk.
46
+ * Real streaming for protocols like SSE or HTTP chunked transfer would be in their specific implementations.
47
+ *
48
+ * @param caller The UTCP client instance.
49
+ * @param toolName Name of the tool to call.
50
+ * @param toolArgs Dictionary of arguments to pass to the tool.
51
+ * @param toolCallTemplate The HTTP call template for the tool.
52
+ * @returns An async generator that yields chunks of the tool's response.
53
+ */
54
+ callToolStreaming(caller: IUtcpClient, toolName: string, toolArgs: Record<string, any>, toolCallTemplate: CallTemplate): AsyncGenerator<any, void, unknown>;
55
+ /**
56
+ * Closes any persistent connections or resources held by the communication protocol.
57
+ * For stateless HTTP, this clears OAuth tokens.
58
+ */
59
+ close(): Promise<void>;
60
+ /**
61
+ * Applies authentication details from the HttpCallTemplate to the Axios request configuration.
62
+ * This modifies `requestConfig.headers`, `requestConfig.params`, `requestConfig.auth`, and returns cookies.
63
+ *
64
+ * @param httpCallTemplate The CallTemplate containing authentication details.
65
+ * @param requestConfig The Axios request configuration to modify.
66
+ * @returns A Promise that resolves to an object containing any cookies to be set.
67
+ */
68
+ private _applyAuthToRequestConfig;
69
+ /**
70
+ * Handles OAuth2 client credentials flow, trying both body and auth header methods.
71
+ * Caches tokens and automatically refreshes if expired.
72
+ *
73
+ * @param authDetails The OAuth2 authentication details.
74
+ * @returns The access token.
75
+ * @throws Error if token cannot be fetched.
76
+ */
77
+ private _handleOAuth2;
78
+ /**
79
+ * Builds a URL by substituting path parameters from the provided arguments.
80
+ * Used arguments are removed from the `args` object.
81
+ *
82
+ * @param urlTemplate The URL template with path parameters in `{param_name}` format.
83
+ * @param args The dictionary of arguments; modified to remove path parameters.
84
+ * @returns The URL with path parameters substituted.
85
+ * @throws Error if a required path parameter is missing.
86
+ */
87
+ private _buildUrlWithPathParams;
88
+ }
@@ -0,0 +1,337 @@
1
+ // packages/http/src/http_communication_protocol.ts
2
+ import axios from 'axios';
3
+ import * as yaml from 'js-yaml';
4
+ import { URLSearchParams } from 'url';
5
+ import { UtcpManualSchema } from '@utcp/core/data/utcp_manual';
6
+ import { HttpCallTemplateSchema } from '@utcp/http/http_call_template';
7
+ import { OpenApiConverter } from '@utcp/http/openapi_converter';
8
+ /**
9
+ * HTTP communication protocol implementation for UTCP client.
10
+ *
11
+ * Handles communication with HTTP-based tool providers, supporting various
12
+ * authentication methods, URL path parameters, and automatic tool discovery.
13
+ * Enforces security by requiring HTTPS or localhost connections.
14
+ */
15
+ export class HttpCommunicationProtocol {
16
+ _oauthTokens = new Map();
17
+ _axiosInstance;
18
+ constructor() {
19
+ this._axiosInstance = axios.create({
20
+ timeout: 30000,
21
+ });
22
+ }
23
+ _logInfo(message) {
24
+ console.log(`[HttpCommunicationProtocol] ${message}`);
25
+ }
26
+ _logError(message, error) {
27
+ console.error(`[HttpCommunicationProtocol Error] ${message}`, error);
28
+ }
29
+ /**
30
+ * Registers a manual and its tools from an HTTP provider.
31
+ * Supports UTCP Manuals directly or OpenAPI specifications which are converted.
32
+ *
33
+ * @param caller The UTCP client instance.
34
+ * @param manualCallTemplate The HTTP call template for discovery.
35
+ * @returns A RegisterManualResult object.
36
+ */
37
+ async registerManual(caller, manualCallTemplate) {
38
+ const httpCallTemplate = HttpCallTemplateSchema.parse(manualCallTemplate);
39
+ try {
40
+ const url = httpCallTemplate.url;
41
+ if (!url.startsWith('https://') && !url.startsWith('http://localhost') && !url.startsWith('http://127.0.0.1')) {
42
+ throw new Error(`Security error: URL must use HTTPS or start with 'http://localhost' or 'http://127.0.0.1'. Got: ${url}. ` +
43
+ "Non-secure URLs are vulnerable to man-in-the-middle attacks.");
44
+ }
45
+ this._logInfo(`Discovering tools from '${httpCallTemplate.name}' (HTTP) at ${url}`);
46
+ const requestConfig = {
47
+ method: httpCallTemplate.http_method,
48
+ url: url,
49
+ headers: { ...httpCallTemplate.headers },
50
+ params: {},
51
+ data: undefined,
52
+ auth: undefined,
53
+ timeout: 10000
54
+ };
55
+ await this._applyAuthToRequestConfig(httpCallTemplate, requestConfig);
56
+ const response = await this._axiosInstance.request(requestConfig);
57
+ const contentType = response.headers['content-type'] || '';
58
+ let responseData;
59
+ if (contentType.includes('yaml') || url.endsWith('.yaml') || url.endsWith('.yml')) {
60
+ responseData = yaml.load(response.data);
61
+ }
62
+ else {
63
+ responseData = response.data;
64
+ }
65
+ let utcpManual;
66
+ if (responseData && responseData.utcp_version && Array.isArray(responseData.tools)) {
67
+ this._logInfo(`Detected UTCP manual from '${httpCallTemplate.name}'.`);
68
+ utcpManual = UtcpManualSchema.parse(responseData);
69
+ }
70
+ else if (responseData && (responseData.openapi || responseData.swagger || responseData.paths)) {
71
+ this._logInfo(`Assuming OpenAPI spec from '${httpCallTemplate.name}'. Converting to UTCP manual.`);
72
+ const converter = new OpenApiConverter(responseData, {
73
+ specUrl: httpCallTemplate.url,
74
+ callTemplateName: httpCallTemplate.name
75
+ });
76
+ utcpManual = converter.convert();
77
+ }
78
+ else {
79
+ throw new Error("Response is neither a valid UTCP Manual nor an OpenAPI Specification.");
80
+ }
81
+ const toolsInManual = utcpManual.tools;
82
+ if (toolsInManual.length > 0) {
83
+ this._logInfo(`Found ${toolsInManual.length} tools.`);
84
+ }
85
+ return {
86
+ manualCallTemplate: httpCallTemplate,
87
+ manual: utcpManual,
88
+ success: true,
89
+ errors: []
90
+ };
91
+ }
92
+ catch (error) {
93
+ this._logError(`Error discovering tools from HTTP provider '${httpCallTemplate.name}':`, error);
94
+ return {
95
+ manualCallTemplate: httpCallTemplate,
96
+ manual: UtcpManualSchema.parse({ tools: [] }),
97
+ success: false,
98
+ errors: [axios.isAxiosError(error) ? error.message : String(error)]
99
+ };
100
+ }
101
+ }
102
+ /**
103
+ * Deregisters an HTTP manual. This is a no-op for stateless HTTP communication.
104
+ * @param caller The UTCP client instance.
105
+ * @param manualCallTemplate The HTTP call template to deregister.
106
+ */
107
+ async deregisterManual(caller, manualCallTemplate) {
108
+ this._logInfo(`Deregistering HTTP manual '${manualCallTemplate.name}' (no-op).`);
109
+ return Promise.resolve();
110
+ }
111
+ /**
112
+ * Executes a tool call through the HTTP protocol.
113
+ *
114
+ * @param caller The UTCP client instance.
115
+ * @param toolName Name of the tool to call.
116
+ * @param toolArgs Dictionary of arguments to pass to the tool.
117
+ * @param toolCallTemplate The HTTP call template for the tool.
118
+ * @returns The tool's response.
119
+ */
120
+ async callTool(caller, toolName, toolArgs, toolCallTemplate) {
121
+ const httpCallTemplate = HttpCallTemplateSchema.parse(toolCallTemplate);
122
+ const requestHeaders = { ...httpCallTemplate.headers };
123
+ let bodyContent = undefined;
124
+ const remainingArgs = { ...toolArgs };
125
+ const queryParams = {};
126
+ if (httpCallTemplate.header_fields) {
127
+ for (const fieldName of httpCallTemplate.header_fields) {
128
+ if (fieldName in remainingArgs) {
129
+ requestHeaders[fieldName] = String(remainingArgs[fieldName]);
130
+ delete remainingArgs[fieldName];
131
+ }
132
+ }
133
+ }
134
+ if (httpCallTemplate.body_field && httpCallTemplate.body_field in remainingArgs) {
135
+ bodyContent = remainingArgs[httpCallTemplate.body_field];
136
+ delete remainingArgs[httpCallTemplate.body_field];
137
+ }
138
+ const url = this._buildUrlWithPathParams(httpCallTemplate.url, remainingArgs);
139
+ Object.assign(queryParams, remainingArgs);
140
+ const requestConfig = {
141
+ method: httpCallTemplate.http_method,
142
+ url: url,
143
+ headers: requestHeaders,
144
+ params: queryParams,
145
+ data: bodyContent,
146
+ auth: undefined,
147
+ timeout: httpCallTemplate.timeout
148
+ };
149
+ await this._applyAuthToRequestConfig(httpCallTemplate, requestConfig);
150
+ try {
151
+ if (bodyContent !== undefined && !('Content-Type' in (requestConfig.headers || {}))) {
152
+ requestConfig.headers = {
153
+ ...(requestConfig.headers || {}),
154
+ 'Content-Type': httpCallTemplate.content_type,
155
+ };
156
+ }
157
+ this._logInfo(`Executing HTTP tool '${toolName}' with URL: ${requestConfig.url} and method: ${requestConfig.method}`);
158
+ const response = await this._axiosInstance.request(requestConfig);
159
+ const contentType = response.headers['content-type'] || '';
160
+ if (contentType.includes('yaml') || url.endsWith('.yaml') || url.endsWith('.yml')) {
161
+ return yaml.load(response.data);
162
+ }
163
+ return response.data;
164
+ }
165
+ catch (error) {
166
+ this._logError(`Error calling HTTP tool '${toolName}':`, error);
167
+ throw error;
168
+ }
169
+ }
170
+ /**
171
+ * Executes a tool call through this transport streamingly.
172
+ * For standard HTTP, this typically means fetching the full response and yielding it as a single chunk.
173
+ * Real streaming for protocols like SSE or HTTP chunked transfer would be in their specific implementations.
174
+ *
175
+ * @param caller The UTCP client instance.
176
+ * @param toolName Name of the tool to call.
177
+ * @param toolArgs Dictionary of arguments to pass to the tool.
178
+ * @param toolCallTemplate The HTTP call template for the tool.
179
+ * @returns An async generator that yields chunks of the tool's response.
180
+ */
181
+ async *callToolStreaming(caller, toolName, toolArgs, toolCallTemplate) {
182
+ this._logInfo(`HTTP protocol does not inherently support streaming for '${toolName}'. Fetching full response.`);
183
+ const result = await this.callTool(caller, toolName, toolArgs, toolCallTemplate);
184
+ yield result;
185
+ }
186
+ /**
187
+ * Closes any persistent connections or resources held by the communication protocol.
188
+ * For stateless HTTP, this clears OAuth tokens.
189
+ */
190
+ async close() {
191
+ this._oauthTokens.clear();
192
+ this._logInfo("HTTP Communication Protocol closed. OAuth tokens cleared.");
193
+ return Promise.resolve();
194
+ }
195
+ /**
196
+ * Applies authentication details from the HttpCallTemplate to the Axios request configuration.
197
+ * This modifies `requestConfig.headers`, `requestConfig.params`, `requestConfig.auth`, and returns cookies.
198
+ *
199
+ * @param httpCallTemplate The CallTemplate containing authentication details.
200
+ * @param requestConfig The Axios request configuration to modify.
201
+ * @returns A Promise that resolves to an object containing any cookies to be set.
202
+ */
203
+ async _applyAuthToRequestConfig(httpCallTemplate, requestConfig) {
204
+ const cookies = {};
205
+ if (httpCallTemplate.auth) {
206
+ if (httpCallTemplate.auth.auth_type === 'api_key') {
207
+ const apiKeyAuth = httpCallTemplate.auth;
208
+ if (!apiKeyAuth.api_key) {
209
+ throw new Error("API key for ApiKeyAuth is empty.");
210
+ }
211
+ if (apiKeyAuth.location === 'header') {
212
+ requestConfig.headers = { ...requestConfig.headers, [apiKeyAuth.var_name]: apiKeyAuth.api_key };
213
+ }
214
+ else if (apiKeyAuth.location === 'query') {
215
+ requestConfig.params = { ...requestConfig.params, [apiKeyAuth.var_name]: apiKeyAuth.api_key };
216
+ }
217
+ else if (apiKeyAuth.location === 'cookie') {
218
+ cookies[apiKeyAuth.var_name] = apiKeyAuth.api_key;
219
+ }
220
+ }
221
+ else if (httpCallTemplate.auth.auth_type === 'basic') {
222
+ const basicAuth = httpCallTemplate.auth;
223
+ requestConfig.auth = {
224
+ username: basicAuth.username,
225
+ password: basicAuth.password
226
+ };
227
+ }
228
+ else if (httpCallTemplate.auth.auth_type === 'oauth2') {
229
+ const oauth2Auth = httpCallTemplate.auth;
230
+ const token = await this._handleOAuth2(oauth2Auth);
231
+ requestConfig.headers = { ...requestConfig.headers, 'Authorization': `Bearer ${token}` };
232
+ }
233
+ }
234
+ return cookies;
235
+ }
236
+ /**
237
+ * Handles OAuth2 client credentials flow, trying both body and auth header methods.
238
+ * Caches tokens and automatically refreshes if expired.
239
+ *
240
+ * @param authDetails The OAuth2 authentication details.
241
+ * @returns The access token.
242
+ * @throws Error if token cannot be fetched.
243
+ */
244
+ async _handleOAuth2(authDetails) {
245
+ const clientId = authDetails.client_id;
246
+ const cachedToken = this._oauthTokens.get(clientId);
247
+ if (cachedToken && cachedToken.expiresAt > Date.now()) {
248
+ return cachedToken.accessToken;
249
+ }
250
+ this._logInfo(`Fetching new OAuth2 token for client: '${clientId}'`);
251
+ const tokenFetchPromises = [];
252
+ // Method 1: Send credentials in the request body
253
+ tokenFetchPromises.push((async () => {
254
+ try {
255
+ const bodyData = new URLSearchParams({
256
+ 'grant_type': 'client_credentials',
257
+ 'client_id': authDetails.client_id,
258
+ 'client_secret': authDetails.client_secret,
259
+ 'scope': authDetails.scope || ''
260
+ });
261
+ const response = await this._axiosInstance.post(authDetails.token_url, bodyData.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
262
+ if (!response.data.access_token) {
263
+ throw new Error("Access token not found in response.");
264
+ }
265
+ const expiresAt = Date.now() + (response.data.expires_in * 1000 || 3600 * 1000);
266
+ this._oauthTokens.set(clientId, { accessToken: response.data.access_token, expiresAt });
267
+ this._logInfo(`OAuth2 token fetched via body for client: '${clientId}'.`);
268
+ return response.data.access_token;
269
+ }
270
+ catch (error) {
271
+ this._logError(`OAuth2 with credentials in body failed for '${clientId}':`, error);
272
+ throw error;
273
+ }
274
+ })());
275
+ // Method 2: Send credentials as Basic Auth header
276
+ tokenFetchPromises.push((async () => {
277
+ try {
278
+ const bodyData = new URLSearchParams({
279
+ 'grant_type': 'client_credentials',
280
+ 'scope': authDetails.scope || ''
281
+ });
282
+ const response = await this._axiosInstance.post(authDetails.token_url, bodyData.toString(), {
283
+ auth: { username: authDetails.client_id, password: authDetails.client_secret },
284
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
285
+ });
286
+ if (!response.data.access_token) {
287
+ throw new Error("Access token not found in response.");
288
+ }
289
+ const expiresAt = Date.now() + (response.data.expires_in * 1000 || 3600 * 1000);
290
+ this._oauthTokens.set(clientId, { accessToken: response.data.access_token, expiresAt });
291
+ this._logInfo(`OAuth2 token fetched via Basic Auth header for client: '${clientId}'.`);
292
+ return response.data.access_token;
293
+ }
294
+ catch (error) {
295
+ this._logError(`OAuth2 with Basic Auth header failed for '${clientId}':`, error);
296
+ throw error;
297
+ }
298
+ })());
299
+ // Try both methods, and resolve if any succeed
300
+ try {
301
+ return await Promise.any(tokenFetchPromises);
302
+ }
303
+ catch (aggregateError) {
304
+ const errorMessages = aggregateError.errors ? aggregateError.errors.map((e) => e.message).join('; ') : String(aggregateError);
305
+ throw new Error(`Failed to fetch OAuth2 token for client '${clientId}' after trying all methods. Details: ${errorMessages}`);
306
+ }
307
+ }
308
+ /**
309
+ * Builds a URL by substituting path parameters from the provided arguments.
310
+ * Used arguments are removed from the `args` object.
311
+ *
312
+ * @param urlTemplate The URL template with path parameters in `{param_name}` format.
313
+ * @param args The dictionary of arguments; modified to remove path parameters.
314
+ * @returns The URL with path parameters substituted.
315
+ * @throws Error if a required path parameter is missing.
316
+ */
317
+ _buildUrlWithPathParams(urlTemplate, args) {
318
+ let url = urlTemplate;
319
+ const pathParams = urlTemplate.match(/\{([^}]+)\}/g) || [];
320
+ for (const param of pathParams) {
321
+ const paramName = param.slice(1, -1);
322
+ if (paramName in args) {
323
+ // URL-encode the parameter value to prevent path injection
324
+ url = url.replace(param, encodeURIComponent(String(args[paramName])));
325
+ delete args[paramName];
326
+ }
327
+ else {
328
+ throw new Error(`Missing required path parameter: ${paramName}`);
329
+ }
330
+ }
331
+ const remainingParams = url.match(/\{([^}]+)\}/g);
332
+ if (remainingParams && remainingParams.length > 0) {
333
+ throw new Error(`Missing required path parameters in URL template: ${remainingParams.join(', ')}`);
334
+ }
335
+ return url;
336
+ }
337
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Registers all HTTP-based protocol CallTemplate serializers
3
+ * and their CommunicationProtocol implementations.
4
+ * This function is called automatically when the package is imported.
5
+ */
6
+ export declare function register(override?: boolean): void;
7
+ export * from './http_call_template';
8
+ export * from './http_communication_protocol';
9
+ export * from './streamable_http_call_template';
10
+ export * from './streamable_http_communication_protocol';
11
+ export * from './sse_call_template';
12
+ export * from './sse_communication_protocol';
13
+ export * from './openapi_converter';
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * HTTP Communication Protocol plugin for UTCP.
3
+ * Includes HTTP, Streamable HTTP, and SSE (Server-Sent Events) protocols.
4
+ */
5
+ // packages/http/src/index.ts
6
+ import { CommunicationProtocol } from '@utcp/core/interfaces/communication_protocol';
7
+ import { CallTemplateSerializer } from '@utcp/core/data/call_template';
8
+ import { ensureCorePluginsInitialized } from '@utcp/core/plugins/plugin_loader';
9
+ import { HttpCallTemplateSerializer } from '@utcp/http/http_call_template';
10
+ import { StreamableHttpCallTemplateSerializer } from '@utcp/http/streamable_http_call_template';
11
+ import { SseCallTemplateSerializer } from '@utcp/http/sse_call_template';
12
+ import { HttpCommunicationProtocol } from '@utcp/http/http_communication_protocol';
13
+ import { StreamableHttpCommunicationProtocol } from '@utcp/http/streamable_http_communication_protocol';
14
+ import { SseCommunicationProtocol } from '@utcp/http/sse_communication_protocol';
15
+ /**
16
+ * Registers all HTTP-based protocol CallTemplate serializers
17
+ * and their CommunicationProtocol implementations.
18
+ * This function is called automatically when the package is imported.
19
+ */
20
+ export function register(override = false) {
21
+ // Ensure core plugins (including auth serializers) are initialized first
22
+ ensureCorePluginsInitialized();
23
+ // Register HTTP
24
+ CallTemplateSerializer.registerCallTemplate('http', new HttpCallTemplateSerializer(), override);
25
+ CommunicationProtocol.communicationProtocols['http'] = new HttpCommunicationProtocol();
26
+ // Register Streamable HTTP
27
+ CallTemplateSerializer.registerCallTemplate('streamable_http', new StreamableHttpCallTemplateSerializer(), override);
28
+ CommunicationProtocol.communicationProtocols['streamable_http'] = new StreamableHttpCommunicationProtocol();
29
+ // Register SSE (Server-Sent Events)
30
+ CallTemplateSerializer.registerCallTemplate('sse', new SseCallTemplateSerializer(), override);
31
+ CommunicationProtocol.communicationProtocols['sse'] = new SseCommunicationProtocol();
32
+ }
33
+ // Automatically register HTTP plugins on import
34
+ register();
35
+ export * from './http_call_template';
36
+ export * from './http_communication_protocol';
37
+ export * from './streamable_http_call_template';
38
+ export * from './streamable_http_communication_protocol';
39
+ export * from './sse_call_template';
40
+ export * from './sse_communication_protocol';
41
+ export * from './openapi_converter';