@utcp/sdk
Version:
Universal Tool Calling Protocol (UTCP) client library for TypeScript
362 lines • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenApiConverter = void 0;
const tool_1 = require("../shared/tool");
const utcp_manual_1 = require("../shared/utcp-manual");
const provider_1 = require("../shared/provider");
const auth_1 = require("../shared/auth");
/**
* Converts an OpenAPI JSON specification into a UtcpManual.
*/
class OpenApiConverter {
/**
* Creates a new OpenAPI converter instance
* @param openapi_spec The OpenAPI specification object
* @param options Optional settings, like the specUrl
*/
constructor(openapi_spec, options) {
this.spec = openapi_spec;
this.specUrl = options?.specUrl;
// If providerName is not provided, get the first word in spec.info.title
if (!options?.providerName) {
const title = openapi_spec.info?.title || 'openapi_provider';
// Replace characters that are invalid for identifiers
const invalidChars = " -.,!?'\"\\/()[]{}#@$%^&*+=~`|;:<>";
this.providerName = title
.split('')
.map((c) => invalidChars.includes(c) ? '_' : c)
.join('');
}
else {
this.providerName = options.providerName;
}
}
/**
* Parses the OpenAPI specification and returns a UtcpManual.
* @returns A UTCP manual containing tools derived from the OpenAPI specification
*/
convert() {
const tools = [];
let baseUrl = '/';
const servers = this.spec.servers;
if (servers && Array.isArray(servers) && servers.length > 0 && servers[0].url) {
baseUrl = servers[0].url;
}
else if (this.specUrl) {
try {
const parsedUrl = new URL(this.specUrl);
baseUrl = `${parsedUrl.protocol}//${parsedUrl.host}`;
}
catch (e) {
console.error(`Invalid specUrl provided: ${this.specUrl}`);
}
}
else {
console.error("No server info or spec URL provided. Using fallback base URL: / ");
}
const paths = this.spec.paths || {};
for (const [path, pathItem] of Object.entries(paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase())) {
const tool = this._createTool(path, method, operation, baseUrl);
if (tool) {
tools.push(tool);
}
}
}
}
return utcp_manual_1.UtcpManualSchema.parse({ tools });
}
/**
* Resolves a local JSON reference.
* @param ref The reference string (e.g. #/components/schemas/Pet)
* @returns The resolved schema object
*/
_resolveRef(ref) {
if (!ref.startsWith('#/')) {
throw new Error(`External or non-local references are not supported: ${ref}`);
}
const parts = ref.substring(2).split('/');
let node = this.spec;
for (const part of parts) {
if (node[part] === undefined) {
throw new Error(`Reference not found: ${ref}`);
}
node = node[part];
}
return node;
}
/**
* Recursively resolves all $refs in a schema object.
* @param schema The schema object that may contain references
* @returns The resolved schema with all references replaced by their actual values
*/
_resolveSchema(schema) {
if (schema === null || typeof schema !== 'object') {
return schema;
}
if (Array.isArray(schema)) {
return schema.map(item => this._resolveSchema(item));
}
if ('$ref' in schema) {
const resolvedRef = this._resolveRef(schema.$ref);
return this._resolveSchema(resolvedRef);
}
const newSchema = {};
for (const [key, value] of Object.entries(schema)) {
newSchema[key] = this._resolveSchema(value);
}
return newSchema;
}
/**
* Creates a Tool object from an OpenAPI operation.
* @param path The API path
* @param method The HTTP method (GET, POST, etc.)
* @param operation The operation definition from OpenAPI
* @param baseUrl The base URL from the servers array
* @returns A Tool object or null if operationId is not defined
*/
_createTool(path, method, operation, baseUrl) {
const operationId = operation.operationId;
if (!operationId) {
return null;
}
const description = operation.summary || operation.description || '';
const tags = operation.tags || [];
const { inputs, header_fields, body_field } = this._extractInputs(operation);
const outputs = this._extractOutputs(operation);
const auth = this._extractAuth(operation);
const providerName = this.spec.info?.title || 'openapi_provider';
const fullUrl = `${baseUrl.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
const provider = provider_1.HttpProviderSchema.parse({
name: providerName,
provider_type: 'http',
http_method: method.toUpperCase(),
url: fullUrl,
body_field: body_field || undefined,
header_fields: header_fields.length > 0 ? header_fields : undefined,
auth
});
return {
name: operationId,
description,
inputs,
outputs,
tags,
tool_provider: provider
};
}
/**
* Extracts the input schema from an OpenAPI operation, resolving refs.
* @param operation The OpenAPI operation object
* @returns The input schema, header fields, and body field
*/
_extractInputs(operation) {
const properties = {};
let required = [];
const header_fields = [];
let body_field = null;
// Handle parameters (path, query, header, cookie)
for (const param of operation.parameters || []) {
const resolvedParam = this._resolveSchema(param);
const paramName = resolvedParam.name;
if (paramName) {
if (resolvedParam.in === 'header') {
header_fields.push(paramName);
}
const schema = this._resolveSchema(resolvedParam.schema || {});
properties[paramName] = {
type: schema.type || 'string',
description: resolvedParam.description || '',
...schema
};
if (resolvedParam.required) {
required.push(paramName);
}
}
}
// Handle request body
const requestBody = operation.requestBody;
if (requestBody) {
const resolvedBody = this._resolveSchema(requestBody);
const content = resolvedBody.content || {};
const jsonSchema = content['application/json']?.schema;
if (jsonSchema) {
body_field = 'body';
properties[body_field] = {
description: resolvedBody.description || 'Request body',
...this._resolveSchema(jsonSchema),
};
if (resolvedBody.required) {
required.push(body_field);
}
}
}
const inputs = tool_1.ToolInputOutputSchema.parse({
properties,
required: required.length > 0 ? required : undefined
});
return { inputs, header_fields, body_field };
}
/**
* Extracts the output schema from an OpenAPI operation, resolving refs.
* @param operation The OpenAPI operation object
* @returns The output schema
*/
_extractOutputs(operation) {
const responses = operation.responses || {};
const successResponse = responses['200'] || responses['201'];
if (!successResponse) {
return tool_1.ToolInputOutputSchema.parse({});
}
const resolvedResponse = this._resolveSchema(successResponse);
const content = resolvedResponse.content || {};
const jsonSchema = content['application/json']?.schema;
if (!jsonSchema) {
return tool_1.ToolInputOutputSchema.parse({});
}
const resolvedJsonSchema = this._resolveSchema(jsonSchema);
const schemaArgs = {
type: resolvedJsonSchema.type || 'object',
properties: resolvedJsonSchema.properties || {},
required: resolvedJsonSchema.required,
description: resolvedJsonSchema.description,
title: resolvedJsonSchema.title
};
// Handle array item types
if (schemaArgs.type === 'array' && 'items' in resolvedJsonSchema) {
schemaArgs.items = resolvedJsonSchema.items;
}
// Handle additional schema attributes
for (const attr of ['enum', 'minimum', 'maximum', 'format']) {
if (attr in resolvedJsonSchema) {
schemaArgs[attr] = resolvedJsonSchema[attr];
}
}
return tool_1.ToolInputOutputSchema.parse(schemaArgs);
}
/**
* Extracts authentication information from OpenAPI operation and global security schemes.
* @param operation The OpenAPI operation object
* @returns An Auth object or undefined if no authentication is specified
*/
_extractAuth(operation) {
// First check for operation-level security requirements
let securityRequirements = operation.security || [];
// If no operation-level security, check global security requirements
if (!securityRequirements.length) {
securityRequirements = this.spec.security || [];
}
// If no security requirements, return undefined
if (!securityRequirements.length) {
return undefined;
}
// Get security schemes - support both OpenAPI 2.0 and 3.0
const securitySchemes = this._getSecuritySchemes();
// Process the first security requirement (most common case)
// Each security requirement is a dict with scheme name as key
for (const securityReq of securityRequirements) {
for (const [schemeName, scopes] of Object.entries(securityReq)) {
if (schemeName in securitySchemes) {
const scheme = securitySchemes[schemeName];
return this._createAuthFromScheme(scheme, schemeName);
}
}
}
return undefined;
}
/**
* Gets security schemes supporting both OpenAPI 2.0 and 3.0.
* @returns A record of security schemes
*/
_getSecuritySchemes() {
// OpenAPI 3.0 format
if ('components' in this.spec) {
return this.spec.components?.securitySchemes || {};
}
// OpenAPI 2.0 format
return this.spec.securityDefinitions || {};
}
/**
* Creates an Auth object from an OpenAPI security scheme.
* @param scheme The security scheme object
* @param schemeName The name of the scheme
* @returns An Auth object or undefined if the scheme is not supported
*/
_createAuthFromScheme(scheme, schemeName) {
const schemeType = (scheme.type || '').toLowerCase();
if (schemeType === 'apikey') {
const location = scheme.in || 'header';
const paramName = scheme.name || 'Authorization';
return auth_1.ApiKeyAuthSchema.parse({
auth_type: 'api_key',
api_key: `\$${this.providerName.toUpperCase()}_API_KEY`,
var_name: paramName,
location,
});
}
if (schemeType === 'basic') {
return auth_1.BasicAuthSchema.parse({
auth_type: 'basic',
username: `\$${this.providerName.toUpperCase()}_USERNAME`,
password: `\$${this.providerName.toUpperCase()}_PASSWORD`,
});
}
if (schemeType === 'http') {
const httpScheme = (scheme.scheme || '').toLowerCase();
if (httpScheme === 'basic') {
return auth_1.BasicAuthSchema.parse({
auth_type: 'basic',
username: `\$${this.providerName.toUpperCase()}_USERNAME`,
password: `\$${this.providerName.toUpperCase()}_PASSWORD`,
});
}
else if (httpScheme === 'bearer') {
return auth_1.ApiKeyAuthSchema.parse({
auth_type: 'api_key',
api_key: `Bearer \$${this.providerName.toUpperCase()}_API_KEY`,
var_name: 'Authorization',
location: 'header',
});
}
}
if (schemeType === 'oauth2') {
const flows = scheme.flows || {};
// OpenAPI 3.0 format
if (Object.keys(flows).length > 0) {
for (const [flowType, flowConfig] of Object.entries(flows)) {
if (['authorizationCode', 'accessCode', 'clientCredentials', 'application'].includes(flowType)) {
const tokenUrl = flowConfig.tokenUrl;
if (tokenUrl) {
const scopes = flowConfig.scopes || {};
return auth_1.OAuth2AuthSchema.parse({
auth_type: 'oauth2',
token_url: tokenUrl,
client_id: `\$${this.providerName.toUpperCase()}_CLIENT_ID`,
client_secret: `\$${this.providerName.toUpperCase()}_CLIENT_SECRET`,
scope: Object.keys(scopes).length > 0 ? Object.keys(scopes).join(' ') : undefined,
});
}
}
}
}
// OpenAPI 2.0 format
else {
const flowType = scheme.flow || '';
const tokenUrl = scheme.tokenUrl;
if (tokenUrl && ['accessCode', 'application', 'clientCredentials'].includes(flowType)) {
return auth_1.OAuth2AuthSchema.parse({
auth_type: 'oauth2',
token_url: tokenUrl,
client_id: `\$${this.providerName.toUpperCase()}_CLIENT_ID`,
client_secret: `\$${this.providerName.toUpperCase()}_CLIENT_SECRET`,
scope: Object.keys(scheme.scopes || {}).length > 0 ? Object.keys(scheme.scopes || {}).join(' ') : undefined,
});
}
}
}
return undefined;
}
}
exports.OpenApiConverter = OpenApiConverter;
//# sourceMappingURL=openapi-converter.js.map