npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

agentic-openapi-parser

v1.7.0

Published

A framework-agnostic TypeScript library for dynamic OpenAPI tool execution

Readme

Agentic OpenAPI 🚀

npm version License: MIT TypeScript API Docs

A highly modular, universal library that dynamically parses OpenAPI/Swagger specifications and turns them into highly structured, AI-ready functions ("Tools"). It securely injects authentication (Bearer, API Keys, Basic Auth, OAuth2) on the fly and provides ready-to-use adapters for the Vercel AI SDK and Langchain.

This library completely eliminates the need to generate static client code. Your AI agent can directly interact with any API given its Swagger/OpenAPI URL!

This README covers usage by example. For the full type reference (every field of ExecuteToolOptions, every exported class/interface across all subpaths), see the generated API docs.


🌟 Key Features

  • Zero Static Code Gen: Fetch, parse, and execute API endpoints directly from an OpenAPI URL or JSON file dynamically.
  • Smart Security Injector: Automatically injects Bearer, API Key, or Basic Auth headers. It can even auto-detect and fix malformed Bearer tokens (e.g., automatically prepending Bearer if the spec or user misses it).
  • AI Ecosystem Adapters: Seamlessly converts API endpoints into DynamicStructuredTool for Langchain or CoreTool for Vercel AI SDK.
  • Strictly Typed & Zero any: Built with uncompromising TypeScript strictness for enterprise stability.
  • Universal Architecture: Usable natively in Vanilla TypeScript/Node.js, or inside an enterprise NestJS backend (via included DynamicModule).

📦 Installation

# Using npm
npm install agentic-openapi

# Using pnpm
pnpm add agentic-openapi

# Using yarn
yarn add agentic-openapi

If you plan to use the AI Adapters, install the peer dependencies you need:

npm install zod @langchain/core ai @modelcontextprotocol/sdk

🔍 CLI — inspect a spec without writing code

npx agentic-openapi-parser inspect https://api.example.com/openapi.json

Prints every tool the spec would flatten into (method, derived name, path), with a warning if the count is high enough to hurt LLM tool-selection accuracy. Useful for a quick sanity check before wiring a spec into your code, or for deciding what to pass to ToolFilterOptions.

# Filter by OpenAPI tag, same as ToolFilterOptions.includeTags/excludeTags
npx agentic-openapi-parser inspect ./openapi.json --tag payments --exclude-tag internal

# Machine-readable output for piping into jq or other tooling
npx agentic-openapi-parser inspect ./openapi.json --json

--tag/--exclude-tag are repeatable. Run npx agentic-openapi-parser --help for the full list.


🚀 Usage Guide

This library uses modern sub-path exports to keep your bundle clean.

1. Vanilla TypeScript Usage (Agent Facade)

You don't need NestJS to use this library. Use the DynamicOpenApiAgent facade for quick integration:

import { DynamicOpenApiAgent } from 'agentic-openapi';

async function run() {
  const agent = new DynamicOpenApiAgent();

  // 1. Parse an OpenAPI spec
  const { spec, tools } = await agent.parseAndFlatten('https://api.example.com/swagger-json');

  // 2. See available tools
  console.log(tools.map(t => t.name)); 
  // Output: ['getUserById', 'createUser', ...]

  // 3. Execute a tool securely
  const result = await agent.executeTool(
    spec, 
    'getUserById', 
    { id: 123 }, // Parameters
    {
      token: 'my-secret-token',
      authType: 'BEARER' // Optional: if omitted, the agent will try to auto-detect from the spec
    }
  );

  console.log(result);
}

2. NestJS Integration

For NestJS apps, the library provides a dynamic module for dependency injection.

// app.module.ts
import { Module } from '@nestjs/common';
import { DynamicOpenApiModule } from 'agentic-openapi/adapters/nestjs';

@Module({
  imports: [
    DynamicOpenApiModule.forRoot()
  ],
})
export class AppModule {}

Then inject the services directly into your controllers/services:

// app.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { 
  OPENAPI_PARSER_SERVICE, 
  IOpenApiParserService,
  DYNAMIC_TOOL_EXECUTOR_SERVICE,
  IDynamicToolExecutorService 
} from 'agentic-openapi/services';

@Injectable()
export class AppService {
  constructor(
    @Inject(OPENAPI_PARSER_SERVICE)
    private parser: IOpenApiParserService,
    @Inject(DYNAMIC_TOOL_EXECUTOR_SERVICE)
    private executor: IDynamicToolExecutorService,
  ) {}

  async doSomething() {
    const { spec, tools } = await this.parser.parseAndFlatten('...');
    // ...
  }
}

🤖 AI Ecosystem Adapters

The true power of this library is feeding OpenAPI specs directly into LLMs!

Vercel AI SDK Adapter

Map your APIs directly into ai compatible tools.

import { DynamicOpenApiAgent } from 'agentic-openapi';
import { VercelAiAdapter } from 'agentic-openapi/adapters/vercel-ai';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';

const agent = new DynamicOpenApiAgent();
const { spec, tools } = await agent.parseAndFlatten('https://api.example.com/docs');

// Create the adapter
const adapter = new VercelAiAdapter(agent.getExecutor(), spec, tools, {
  token: 'YOUR_API_KEY',
  authType: 'BEARER'
});

// Pass directly to Vercel AI
const result = await generateText({
  model: openai('gpt-4-turbo'),
  prompt: 'Get the user with ID 123',
  tools: adapter.getTools()
});

Langchain Adapter

Seamlessly integrate with Langchain's agents and tools ecosystem.

import { DynamicOpenApiAgent } from 'agentic-openapi';
import { LangchainToolAdapter } from 'agentic-openapi/adapters/langchain';
import { ChatOpenAI } from '@langchain/openai';
import { createOpenAIToolsAgent, AgentExecutor } from 'langchain/agents';

const agent = new DynamicOpenApiAgent();
const { spec, tools } = await agent.parseAndFlatten('https://api.example.com/docs');

const adapter = new LangchainToolAdapter(agent.getExecutor(), spec, tools, {
  token: 'YOUR_API_KEY',
  authType: 'BEARER'
});

const llm = new ChatOpenAI({ modelName: "gpt-4" });
const langchainTools = adapter.getTools(); // Returns DynamicStructuredTool[]

// Bind tools to the LLM
const llmWithTools = llm.bindTools(langchainTools);

MCP Adapter

Expose your parsed OpenAPI tools on an MCP server. This adapter only registers tools — it does not choose a transport or call .connect(), so you stay in control of whether the server runs over stdio, HTTP, or anything else.

import { DynamicOpenApiAgent } from 'agentic-openapi';
import { McpToolAdapter } from 'agentic-openapi/adapters/mcp';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const agent = new DynamicOpenApiAgent();
const { spec, tools } = await agent.parseAndFlatten('https://api.example.com/docs');

const adapter = new McpToolAdapter(agent.getExecutor(), spec, tools, {
  token: 'YOUR_API_KEY',
  authType: 'BEARER'
});

// Builds a McpServer and registers every tool onto it (unconnected)
const server = adapter.createServer({ name: 'my-api-server', version: '1.0.0' });

// You choose the transport and connect it yourself
const transport = new StdioServerTransport();
await server.connect(transport);

If you already have a McpServer instance (e.g. one that also serves resources/prompts), use adapter.registerOn(existingServer) instead of createServer().

OpenAI / Anthropic Native Tool Adapters

Both providers' tool-calling format is plain JSON — no SDK required, so these two adapters carry zero peer dependency. getTools() returns wire-format objects ready to pass straight into the provider's request; executeToolCall(name, args) maps a returned tool call back to the matching OpenAPI operation and runs it.

import { DynamicOpenApiAgent } from 'agentic-openapi';
import { OpenAiToolAdapter } from 'agentic-openapi/adapters/openai';
import OpenAI from 'openai';

const agent = new DynamicOpenApiAgent();
const { spec, tools } = await agent.parseAndFlatten('https://api.example.com/docs');
const adapter = new OpenAiToolAdapter(agent.getExecutor(), spec, tools, { accessToken: 'YOUR_API_KEY' });

const client = new OpenAI();
const response = await client.chat.completions.create({
  model: 'gpt-4-turbo',
  messages: [{ role: 'user', content: 'Get the user with ID 123' }],
  tools: adapter.getTools(),
});

const call = response.choices[0]?.message.tool_calls?.[0];
if (call) {
  const result = await adapter.executeToolCall(call.function.name, JSON.parse(call.function.arguments));
}

AnthropicToolAdapter (agentic-openapi/adapters/anthropic) works the same way — getTools() returns { name, description, input_schema }[] for the Messages API's tools field, and a tool_use content block's name/input go straight into executeToolCall(name, input).


🧠 Request Bodies & Nested Schemas

Tool schemas aren't limited to flat query/path parameters — requestBody (OpenAPI 3), nested objects, arrays, enums, and common string formats (date-time, email, uuid) are all converted into the matching Zod validators, so the LLM sees (and is validated against) the real shape of the payload it needs to send, not just a generic string/object. A POST endpoint with a JSON request body shows up in the generated tool schema as a requestBody field alongside the usual parameters.


🧰 Tool Filtering

Large specs (Stripe, GitHub, ...) can flatten into hundreds of tools, which hurts LLM tool-selection accuracy well before it hits any provider limit. Pass a ToolFilterOptions as the third argument to parseAndFlatten() to keep only the tools you actually want to expose:

const { tools } = await agent.parseAndFlatten('https://api.example.com/openapi.json', 'stripe', {
  includeTags: ['payments', 'customers'],   // keep only these OpenAPI tags
  excludeOperationIds: ['admin*'],          // glob against the derived tool name
  excludePaths: ['/internal/**'],           // glob against the OpenAPI path template
});

All four dimensions (includeTags/excludeTags, includeOperationIds/excludeOperationIds, includePaths/excludePaths) are optional and combine with AND semantics — a tool must pass every filter you specify. include* uses OR-within-dimension (any pattern matching is enough), exclude* drops a tool if any pattern matches. Patterns support * (single path segment), ** (any number of segments), and ? (single character).


🗄️ Spec Caching & Multi-Spec Namespacing

Caching — re-parsing and re-dereferencing a large spec on every parseAndFlatten() call is wasteful for a long-lived process. Opt in via the OpenApiParserService constructor (this is a policy for the parser instance, like maxConcurrency is for the executor):

import { OpenApiParserService } from 'agentic-openapi-parser';

const parser = new OpenApiParserService(logger, {
  cache: {
    ttlMs: 5 * 60 * 1000,      // how long a dereferenced document stays fresh
    revalidateWithEtag: true,  // default true — see below
  },
});

For http(s):// spec URLs, once the TTL expires the parser doesn't necessarily redo the full dereference: if the server returned an ETag on the last fetch, it sends a conditional If-None-Match request first — a 304 Not Modified reuses the already-dereferenced document at almost no cost, and only an actual 200 with new content triggers a full re-parse. Local file paths (or an already-parsed object) skip the ETag step entirely (there's no HTTP response to revalidate against) but still benefit from the TTL window itself.

Multi-spec namespacing — when you flatten tools from more than one spec into a single list for an LLM, two specs can derive the same tool name (two getUser operations). Pass a namespace as the fourth argument to parseAndFlatten() to prefix every tool name from that spec:

const { tools: githubTools } = await agent.parseAndFlatten(githubSpecUrl, 'github-provider', undefined, 'github');
const { tools: stripeTools } = await agent.parseAndFlatten(stripeSpecUrl, 'stripe-provider', undefined, 'stripe');
// githubTools[0].name === 'github__getUser', stripeTools[0].name === 'stripe__getUser' — no collision

Pass the same namespace back in ExecuteToolOptions.namespace when executing one of these tools, so the executor can strip the prefix and resolve the original OpenAPI operation:

await agent.executeTool(spec, 'github__getUser', args, { namespace: 'github', accessToken: '...' });

🔁 Retry, Timeout & Concurrency

Third-party APIs are flaky. Three independent knobs handle this without any extra dependency:

  • Per-call timeoutoptions.timeout (ms) on executeTool()/execute(), already scoped to a single tool call so different endpoints on the same provider can use different budgets.

  • Retry with backoffoptions.retry on the same call:

    await agent.executeTool(spec, 'getInvoice', args, {
      retry: {
        maxRetries: 3,               // default 0 (no retry)
        retryDelayMs: 300,           // base delay; exponential backoff with full jitter
        retryableStatusCodes: [408, 429, 500, 502, 503, 504], // this is the default
        retryOnNetworkError: true,   // retry when there's no HTTP response at all
      },
    });

    Only retries idempotent-looking failures (timeouts, 429/5xx, connection resets) — a 4xx client error fails immediately, no retry. When a retryable failure's response carries a Retry-After header (seconds or an HTTP-date — GitHub and Stripe both send this on 429s), that value is used as-is instead of the exponential backoff above, since the server is telling you exactly how long to wait.

  • Concurrency limit — a policy for the executor instance rather than a single call, since it protects one provider's API from a burst of parallel tool calls (e.g. an LLM turn requesting 20 tools at once):

    import { DynamicToolExecutorService, OpenApiSecurityInjector } from 'agentic-openapi-parser';
    
    const executor = new DynamicToolExecutorService(new OpenApiSecurityInjector(logger), logger, {
      maxConcurrency: 5, // extra calls queue instead of firing all at once
    });

    Pass this executor into DynamicOpenApiAgent's constructor overrides (see Overriding facade pieces) to wire it into the facade.

  • executeMany for parallel tool calls — an LLM turn often returns several tool_calls at once (OpenAI and Anthropic both support this). executeMany runs them concurrently through the same execute() path — so retry, security injection, response processors, and the concurrency limit above all apply per call — and returns one outcome per call, in the same order they were given, without letting one failure affect the others:

    const outcomes = await agent.executeMany(spec, [
      { toolName: 'getInvoice', args: { id: 1 } },
      { toolName: 'getInvoice', args: { id: 2 } },
    ]);
    
    for (const outcome of outcomes) {
      if (outcome.status === 'fulfilled') {
        console.log(outcome.toolName, outcome.value);
      } else {
        console.error(outcome.toolName, outcome.reason); // e.g. a ToolExecutionError
      }
    }

    This mirrors the shape of Promise.allSettled (status/value/reason) plus a toolName so you don't have to cross-reference the original call array by index.


📡 Observability Hooks

Production needs to know which tool is slow, which one keeps failing, and what's in flight right now. options.hooks adds three synchronous callbacks, so you can wire this into OpenTelemetry, Datadog, or a plain log line — the library has no opinion on which:

await agent.executeTool(spec, 'getInvoice', args, {
  hooks: {
    onRequestStart: ({ requestId, toolName }) => span.start(requestId, toolName),
    onRequestEnd: ({ requestId, durationMs, success, statusCode }) => span.end(requestId, { durationMs, success, statusCode }),
    onRetry: ({ toolName, attempt, statusCode, delayMs }) => metrics.increment('tool_retry', { toolName, attempt, statusCode, delayMs }),
  },
});

Every event for one call carries the same requestId, so onRequestStart/onRequestEnd/onRetry can be correlated even when executeMany runs the same tool name concurrently more than once. A hook that throws is caught and logged — it can never fail or slow down the actual tool call.


🔑 OAuth2 Token Management

Two ExecuteToolOptions hooks cover the two OAuth2 grant types this library supports end-to-end. Both are deliberately narrow in scope — authorization_code/PKCE and refresh_token rotation are left to the caller, since they involve app-specific concerns (user sessions, redirect handling) this library has no business owning.

client_credentials (machine-to-machine, no user/redirect involved) — use ClientCredentialsTokenProvider when the API itself issues short-lived tokens from a client id/secret pair:

import { ClientCredentialsTokenProvider } from 'agentic-openapi-parser';

const accessTokenProvider = new ClientCredentialsTokenProvider({
  tokenUrl: 'https://provider.example.com/oauth/token',
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  scope: 'read write', // optional
});

await agent.executeTool(spec, 'getInvoice', args, { accessTokenProvider });

It fetches a token on first use, caches it in memory, and transparently fetches a new one once the cached token is within 5 minutes of expiry (refreshThresholdMs, configurable). Concurrent calls while a fetch is in flight share the same request instead of hammering the token endpoint. If a renewal fetch fails but a still-valid cached token exists, it keeps serving that token (and logs a warning); if there's no usable token at all, executeTool() rejects with a clear error rather than silently sending an unauthenticated request.

refresh_token (a user-specific token you already have, refreshed as it nears expiry) — use Oauth2RefreshTokenRefresher together with oauth2State:

import { Oauth2RefreshTokenRefresher } from 'agentic-openapi-parser';

const tokenRefresher = new Oauth2RefreshTokenRefresher({
  onRefreshed: (newState) => saveTokenToYourDatabase(newState), // fire-and-forget
});

await agent.executeTool(spec, 'getInvoice', args, {
  accessToken: currentAccessToken,
  tokenRefresher,
  oauth2State: { accessToken: currentAccessToken, refreshToken, tokenUrl, tokenExpiresAt, clientId, clientSecret },
});

If both accessTokenProvider and tokenRefresher/oauth2State are set, accessTokenProvider wins — they model different grant types and aren't meant to be combined on the same call.


✂️ Response Shaping

Two composable ResponseProcessors (same pipeline as TruncateResponseProcessor) address the two distinct causes of oversized tool responses:

JmesPathSelectProcessor — pick out / reshape only the fields the LLM actually needs, using a JMESPath expression:

import { JmesPathSelectProcessor } from 'agentic-openapi-parser';

await agent.executeTool(spec, 'listOrders', args, {
  responseProcessors: [new JmesPathSelectProcessor('orders[*].{id: id, total: total, status: status}')],
});

JMESPath, not JSONPath, on purpose: it's a fully declarative query language with no script/filter syntax that can evaluate arbitrary code — this matters because the expression may end up coming from semi-trusted, caller-supplied config rather than only from a developer's own source. A malformed expression throws immediately (it's a configuration bug, not a transient failure) rather than silently passing the response through unshaped.

MaxBytesResponseProcessor — a last-resort cap on the total serialized response size, independent of TruncateResponseProcessor (which bounds individual strings/arrays, not the whole payload):

import { MaxBytesResponseProcessor } from 'agentic-openapi-parser';

await agent.executeTool(spec, 'getReport', args, {
  responseProcessors: [new MaxBytesResponseProcessor(50 * 1024)], // default is 50 KB
});

Over the limit, it returns { truncated: true, originalSizeBytes, maxBytes, preview } instead of the raw value — preview is cut at a byte-accurate boundary that never splits a multi-byte UTF-8 character. Order processors deliberately: put JmesPathSelectProcessor first to shrink the payload semantically, MaxBytesResponseProcessor last as the final safety net.

A processor that throws (a bad JMESPath expression, a bug in your own custom processor) surfaces as its own error — it is never mislabeled as a failed HTTP request, since post-processing runs outside the request/retry error handling.


🔎 Semantic Tool Search

ToolFilterOptions (see "Tool Filtering" above) is a static filter — the same rules apply to every request. Once a spec is large enough (100-200+ tools) that no single static filter serves every user intent well, SemanticToolIndex ranks tools by similarity to the specific query at hand instead:

import { SemanticToolIndex } from 'agentic-openapi-parser';

// Bring your own embedding call — SemanticToolIndex has no opinion on which provider you use.
const embeddingProvider = {
  embed: async (texts: string[]) => {
    const response = await openai.embeddings.create({ model: 'text-embedding-3-small', input: texts });
    return response.data.map((d) => d.embedding);
  },
};

const { tools } = await agent.parseAndFlatten('https://api.example.com/openapi.json');
const index = new SemanticToolIndex(embeddingProvider);
await index.build(tools); // embeds every tool once

const relevantTools = await index.search('refund a customer payment', 10); // top 10 by cosine similarity
const adapter = new LangchainToolAdapter(agent.getExecutor(), spec, relevantTools);

SemanticToolIndex deliberately never calls an embedding API itself — EmbeddingProvider is the same bring-your-own-X pattern used for OAuth2 tokens and response processors, so this library isn't hard-wired to OpenAI, Cohere, or any specific model. It owns only what's genuinely fiddly to get right: embedding each tool exactly once, and ranking many search() calls against that index by cosine similarity without re-embedding anything. By default each tool is embedded as "name description tag1 tag2 ..." — pass buildText to customize that.


🧪 Testing Helpers

Code that wires an adapter (Langchain, Vercel AI, MCP, OpenAI, Anthropic) to this library still needs a IDynamicToolExecutorService to construct it. createMockExecutor fakes one from a plain map of tool name → response, so a consumer's own tests don't need a real spec, a real HTTP call, or mocking this library's internals directly:

import { createMockExecutor } from 'agentic-openapi-parser/testing';
import { LangchainToolAdapter } from 'agentic-openapi-parser/adapters/langchain';

const executor = createMockExecutor({
  getPet: { id: 1, name: 'Rex' },
  createPet: (args: Record<string, unknown>) => ({ id: 99, ...args }),
  deletePet: new Error('upstream 500'), // simulates a failed tool call
});

const adapter = new LangchainToolAdapter(executor, spec, tools);
// ...invoke a tool through the adapter, then assert on executor.calls:
expect(executor.calls).toEqual([{ toolName: 'getPet', args: { petId: 1 }, options: undefined }]);

A tool name missing from the response map throws the same ToolNotFoundError the real executor would, so error-path tests stay realistic. A response can be a static value, a (args, options) => value function for dynamic responses, or an Error instance to simulate a failed call.

If instead you want to test this library's own HTTP behavior (retries, timeouts, real request shape) rather than mock it away, intercept at the HTTP layer with nock, msw, or axios-mock-adapter instead — createMockExecutor is deliberately for the opposite case, where the executor itself is a dependency you want to stub out.


🚨 Error Handling

Every error this library throws deliberately extends AgenticOpenApiError (itself a real Error), instead of a generic new Error(message) a consumer can only distinguish by parsing the message string:

import { AgenticOpenApiError, ToolNotFoundError, ToolExecutionError } from 'agentic-openapi-parser';

try {
  await agent.executeTool(spec, 'getInvoice', args);
} catch (error) {
  if (error instanceof ToolNotFoundError) {
    console.error(`No such tool: ${error.toolName}`);
  } else if (error instanceof ToolExecutionError) {
    console.error(`API call failed with status ${error.statusCode}`, error.responseData);
  } else if (error instanceof AgenticOpenApiError) {
    console.error('Something else this library rejected on purpose:', error.message);
  } else {
    throw error; // a genuinely unexpected bug — don't swallow it
  }
}

| Class | Thrown by | Structured fields | |---|---|---| | SpecParseError | parseAndFlatten() when the spec can't be fetched/dereferenced | apiSpecUrl | | ToolNotFoundError | executeTool() / a native adapter's executeToolCall(), unknown tool name | toolName | | ToolExecutionError | executeTool(), the HTTP request itself failed | statusCode, responseData | | ResponseProcessingError | executeTool(), a ResponseProcessor threw | processorName | | AccessTokenError | ClientCredentialsTokenProvider, no usable token could be obtained | — | | EmbeddingProviderError | SemanticToolIndex, the supplied EmbeddingProvider misbehaved | — |

Every subclass also carries the original failure via the standard cause property (error.cause), so nothing is lost by wrapping.


🔒 Security & Logging

This library implements robust safety checks:

  • Zero Console.log: Uses pino logger via DI to ensure structured, production-ready logging.
  • Token Masking: Securely masks API keys and Bearer tokens in logs (Bearer my-s******oken) to prevent credential leakage.
  • Strict Null Checks: Enforces strict array and object indexing to prevent runtime crashes from malformed specs.

🧩 Extending

The library is built around small, swappable pieces. See CONTRIBUTING.md for the full walkthrough — the short version:

Custom auth strategy

Security injection is a registry of ISecurityStrategy implementations keyed by OpenAPI securityScheme.type (apiKey, http, oauth2, basic, ...). Add your own without touching the built-in ones:

import { OpenApiSecurityInjector, SecurityStrategyRegistry, createDefaultSecurityStrategyRegistry } from 'agentic-openapi/services';

class HmacSignatureStrategy /* implements ISecurityStrategy */ {
  schemeTypes = ['x-hmac'];
  supportsAuthType() { return true; }
  inject({ headers, accessToken }) {
    headers['X-Signature'] = signRequest(accessToken);
    return true;
  }
}

const registry = createDefaultSecurityStrategyRegistry().register(new HmacSignatureStrategy());
const securityInjector = new OpenApiSecurityInjector(logger, registry);

Custom AI adapter

This is for contributors adding a new adapter inside the repo (see CONTRIBUTING.md) — extend BaseAiAdapter (src/adapters/shared/base-ai-adapter.ts) to add support for another framework; it already gives you schema generation, safe tool naming, and executor wiring:

import { BaseAiAdapter } from '@/adapters/shared';

class MyFrameworkAdapter extends BaseAiAdapter<MyToolShape, MyToolShape[]> {
  getTools() {
    return this.toolsDef.map((toolDef) => ({
      name: this.safeToolName(toolDef.name),
      schema: this.buildSchema(toolDef.parameters),
      execute: (args) => this.run(toolDef.name, args),
    }));
  }
}

Overriding facade pieces

DynamicOpenApiAgent accepts optional service overrides as a second constructor argument, so you can swap in a custom parser/injector/executor without bypassing the facade:

const agent = new DynamicOpenApiAgent(logger, { securityInjector });

License

MIT