openapi-explorer-mcp 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eugene Trofimov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # openapi-explorer-mcp
2
+
3
+ An MCP server for any OpenAPI 3 spec. It lets an AI agent find endpoints, inspect request and response shapes
4
+ without loading a megabyte of JSON, generate TypeScript types, and call endpoints — with credentials mapped to the
5
+ security schemes the spec already declares.
6
+
7
+ Unlike servers that turn every operation into its own tool, this one stays small: a handful of tools that explore
8
+ the spec and one generic caller.
9
+
10
+ ## Tools
11
+
12
+ | Tool | What it does |
13
+ |---|---|
14
+ | `api_spec_info` | Spec version and age, counts, groups, security schemes with credential status, changes since the previous version. |
15
+ | `api_search` | Searches method, path, operationId, summary, tags and parameter names. |
16
+ | `api_endpoint` | Parameters, request and response shapes (compact, depth-limited), danger level, security alternatives, URL. |
17
+ | `api_schema` | A component schema by name, with drill-down into nested fields and the endpoints that use it. |
18
+ | `api_types` | TypeScript types for an endpoint's request, response and parameters, generated with `@hey-api/openapi-ts`. |
19
+ | `api_get` | Calls a GET endpoint. |
20
+ | `api_request` | Calls an endpoint with any method. Registered only with `OPENAPI_ALLOW_WRITE`; destructive endpoints need `confirm_danger: true`. |
21
+ | `api_call_log` | Journal of `api_request` calls with ids from responses, for cleaning up. |
22
+ | `api_auth` | Mints tokens through the auth module. Registered only when the module supports it. |
23
+ | `recipe` | Markdown recipes for this API. Registered only with `OPENAPI_RECIPES_DIR`. |
24
+
25
+ ## Configuration
26
+
27
+ | Variable | |
28
+ |---|---|
29
+ | `OPENAPI_SPEC_URL` | Required. URL or file path of an OpenAPI 3 JSON spec. URLs are cached on disk and revalidated with ETag. |
30
+ | `OPENAPI_BASE_URL` | Base URL for calls. Required whenever any credential or header is configured; otherwise `servers[0].url` of the spec is used for anonymous calls. |
31
+ | `OPENAPI_AUTH_<SCHEME>` | Credential for a security scheme — see [Authentication](#authentication). |
32
+ | `OPENAPI_HEADER_<NAME>` | A header sent with every call, for specs that don't declare security schemes. `OPENAPI_HEADER_X_API_KEY` sends `x-api-key`. |
33
+ | `OPENAPI_AUTH_MODULE` | Path to an ES module that supplies credentials minted at runtime. |
34
+ | `OPENAPI_ENV_FILE` | Env file merged into the environment at startup; variables already set win. |
35
+ | `OPENAPI_ALLOW_WRITE` | `1`, `true` or `yes` registers `api_request`. Off by default. |
36
+ | `OPENAPI_DANGER_FILE` | JSON with danger overrides — see [Danger rules](#danger-rules). |
37
+ | `OPENAPI_RECIPES_DIR` | Directory of markdown recipes (with a `description:` line) served by `recipe`. |
38
+ | `OPENAPI_INSTRUCTIONS_FILE` | Markdown appended to the instructions the server gives the model. |
39
+ | `OPENAPI_SERVER_NAME` | Server name reported to the client. Default `openapi`. |
40
+ | `OPENAPI_CACHE_DIR` | Spec cache and generated types. Default `~/.cache/openapi-explorer-mcp/<hash of the spec source>`. |
41
+ | `OPENAPI_CALL_LOG` | Journal of `api_request` calls. Default `<cache dir>/calls.jsonl`. |
42
+ | `OPENAPI_SPEC_TTL_S` | How often a URL spec is revalidated. Default `900`. |
43
+ | `OPENAPI_TIMEOUT_MS` | Timeout of spec fetches and calls. Default `20000`. |
44
+ | `OPENAPI_MAX_RESPONSE_CHARS` | Cap on a tool response. Default `40000`. |
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "my-api": {
50
+ "command": "npx",
51
+ "args": ["-y", "openapi-explorer-mcp"],
52
+ "env": {
53
+ "OPENAPI_SPEC_URL": "https://api.example.com/openapi.json",
54
+ "OPENAPI_BASE_URL": "https://api.example.com",
55
+ "OPENAPI_AUTH_X_API_KEY": "${MY_API_KEY}",
56
+ "OPENAPI_SERVER_NAME": "my-api"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Authentication
64
+
65
+ The server doesn't invent headers — it reads them from the spec. `components.securitySchemes` says where a secret
66
+ goes, and each operation's `security` says which schemes it accepts. You only give a scheme its value.
67
+
68
+ **Credentials.** `OPENAPI_AUTH_<SCHEME>` holds the value for a scheme; the name is upper-cased with every other
69
+ character replaced by `_`: `x-admin-token` → `OPENAPI_AUTH_X_ADMIN_TOKEN`, `bearer` → `OPENAPI_AUTH_BEARER`. The
70
+ value is placed where the scheme says:
71
+
72
+ | Scheme | Placement |
73
+ |---|---|
74
+ | `apiKey` in `header` / `query` / `cookie` | the named header, query parameter or cookie |
75
+ | `http` `bearer`, `oauth2`, `openIdConnect` | `Authorization: Bearer <value>` |
76
+ | `http` `basic` | `Authorization: Basic …` — give `user:password` or an already encoded value |
77
+
78
+ **Which scheme a call uses.** `security` is a list of alternatives. With `as: "auto"` (the default) the server
79
+ takes the first alternative whose schemes all have credentials. `as` can also name a scheme to force it, or be
80
+ `"anonymous"`. When nothing is configured, a GET is sent anonymously with a note (many GET endpoints declare auth
81
+ but also answer without it); any other method fails with the name of the variable to set.
82
+
83
+ **Tokens minted at runtime.** `OPENAPI_AUTH_MODULE` points to an ES module whose default export creates a provider.
84
+ The `identity` argument of `api_get` and `api_request` is passed to it as is. Types are exported by the package:
85
+
86
+ ```ts
87
+ import type { AuthProviderFactory } from 'openapi-explorer-mcp';
88
+
89
+ const createAuth: AuthProviderFactory = ({ baseUrl, timeoutMs, env }) => ({
90
+ canProvide: (scheme, { identity }) => scheme === 'bearer' && Boolean(identity ?? env.DEFAULT_USER),
91
+ getCredential: async (scheme, { identity, force }) => mintToken(baseUrl, identity ?? env.DEFAULT_USER, { force, timeoutMs }),
92
+ // optional: registers api_auth
93
+ authenticate: async ({ identity, force }) => ({ identity: identity ?? 'default', accessToken: await mintToken(/* … */) }),
94
+ });
95
+
96
+ export default createAuth;
97
+ ```
98
+
99
+ A static credential from `OPENAPI_AUTH_<SCHEME>` wins over the module for the same scheme. When a call that used a
100
+ module credential gets `401`, the server asks the module again with `force: true` and retries once.
101
+
102
+ **What keeps credentials safe**
103
+
104
+ - Only the person configuring the server sets values; no tool accepts headers or tokens, so the model picks a
105
+ scheme, never a value.
106
+ - Credentials go only to `OPENAPI_BASE_URL`, which must be set explicitly when any credential exists. The spec's
107
+ `servers` is never trusted with them — the spec is fetched over the network and could point elsewhere.
108
+ - The origin of every request is checked against the base URL before sending; path parameters are URL-encoded.
109
+ - Values never appear in tool output or in the call journal: responses name the scheme, and `api_spec_info` shows
110
+ only whether a scheme has a credential.
111
+
112
+ ## Danger rules
113
+
114
+ Every non-GET operation is `write`, and `destructive` when it is a `DELETE` or its path contains `drop`, `purge`,
115
+ `reset`, `destroy`, `bulk` or `broadcast`. `api_request` refuses destructive operations without
116
+ `confirm_danger: true`. `OPENAPI_DANGER_FILE` adds exact operations and path words:
117
+
118
+ ```json
119
+ {
120
+ "operations": {
121
+ "POST /orders": "creates a real order"
122
+ },
123
+ "pathPatterns": ["close", "withdraw"]
124
+ }
125
+ ```
126
+
127
+ ## Development
128
+
129
+ ```
130
+ npm install
131
+ npm run typecheck
132
+ npm run build # tsc into dist/
133
+ npm run smoke # stdio checks against scripts/fixtures/pets.json, no network
134
+ npm run check # all three
135
+ ```
136
+
137
+ ## License
138
+
139
+ MIT — see [LICENSE](LICENSE).
package/dist/auth.d.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { type ExplorerConfig } from './config.js';
2
+ import type { Operation, SecurityScheme } from './spec-index.js';
3
+ /**
4
+ * Per-call context passed to an auth provider.
5
+ */
6
+ export interface AuthContext {
7
+ /** Identity from the tool call, e.g. a user id; the provider decides what it means. */
8
+ identity?: string;
9
+ /** Ignore cached credentials, e.g. after a 401. */
10
+ force?: boolean;
11
+ }
12
+ /**
13
+ * Tokens a provider minted, shown by the api_auth tool.
14
+ */
15
+ export interface AuthSession {
16
+ /** Who the tokens belong to. */
17
+ identity: string;
18
+ /** When the access token expires, ISO 8601. */
19
+ expiresAt?: string;
20
+ /** Access token. */
21
+ accessToken?: string;
22
+ /** Refresh token. */
23
+ refreshToken?: string;
24
+ }
25
+ /**
26
+ * Supplies credentials that can't be static, e.g. tokens minted per user.
27
+ */
28
+ export interface AuthProvider {
29
+ /** Whether the provider can supply a credential for the scheme in this context, without doing I/O. */
30
+ canProvide(scheme: string, context: AuthContext): boolean;
31
+ /** Returns the credential value for the scheme. */
32
+ getCredential(scheme: string, context: AuthContext): Promise<string>;
33
+ /** Mints or refreshes tokens for an identity; registers the api_auth tool when present. */
34
+ authenticate?(context: AuthContext): Promise<AuthSession>;
35
+ }
36
+ /**
37
+ * What a provider factory receives.
38
+ */
39
+ export interface AuthProviderOptions {
40
+ /** OPENAPI_BASE_URL. */
41
+ baseUrl: string;
42
+ /** OPENAPI_TIMEOUT_MS. */
43
+ timeoutMs: number;
44
+ /** The process environment, with OPENAPI_ENV_FILE already merged in. */
45
+ env: NodeJS.ProcessEnv;
46
+ }
47
+ /** The default export of an OPENAPI_AUTH_MODULE. */
48
+ export type AuthProviderFactory = (options: AuthProviderOptions) => AuthProvider | Promise<AuthProvider>;
49
+ /** Which security alternative a call uses. */
50
+ export type AuthSelection = {
51
+ mode: 'anonymous';
52
+ note?: string;
53
+ } | {
54
+ mode: 'credentials';
55
+ schemes: string[];
56
+ };
57
+ /**
58
+ * Imports OPENAPI_AUTH_MODULE and creates its provider.
59
+ */
60
+ export declare function loadAuthProvider(config: ExplorerConfig): Promise<AuthProvider | undefined>;
61
+ /**
62
+ * Maps credentials from the environment and an auth provider onto the security schemes of the spec.
63
+ */
64
+ export declare class Credentials {
65
+ private readonly config;
66
+ private readonly provider?;
67
+ constructor(config: ExplorerConfig, provider?: AuthProvider | undefined);
68
+ /**
69
+ * Where the credential of a scheme comes from, or null when nothing supplies it.
70
+ */
71
+ source(scheme: string, context?: AuthContext): 'env' | 'module' | null;
72
+ /**
73
+ * Credential status of a scheme for spec info.
74
+ */
75
+ describe(scheme: string): string;
76
+ /**
77
+ * Picks the security alternative for a call: a forced scheme, the first alternative with all credentials, or anonymous.
78
+ */
79
+ select(op: Operation, as: string, context: AuthContext, schemes: Record<string, SecurityScheme>): AuthSelection;
80
+ /**
81
+ * Puts the credentials of the selected schemes into the request; reports whether any came from the provider.
82
+ */
83
+ apply(selection: AuthSelection, schemes: Record<string, SecurityScheme>, context: AuthContext, headers: Record<string, string>, url: URL): Promise<{
84
+ fromProvider: boolean;
85
+ }>;
86
+ /**
87
+ * Tells the user how to supply a credential for a scheme.
88
+ */
89
+ private hint;
90
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,140 @@
1
+ import { pathToFileURL } from 'node:url';
2
+ import { ConfigError, schemeEnvName, schemeEnvSuffix } from './config.js';
3
+ /**
4
+ * Imports OPENAPI_AUTH_MODULE and creates its provider.
5
+ */
6
+ export async function loadAuthProvider(config) {
7
+ if (!config.authModule || !config.baseUrl)
8
+ return undefined;
9
+ const mod = (await import(pathToFileURL(config.authModule).href));
10
+ if (typeof mod.default !== 'function') {
11
+ throw new ConfigError(`OPENAPI_AUTH_MODULE must default-export a factory function: ${config.authModule}`);
12
+ }
13
+ const provider = (await mod.default({ baseUrl: config.baseUrl, timeoutMs: config.timeoutMs, env: process.env }));
14
+ if (!provider || typeof provider.canProvide !== 'function' || typeof provider.getCredential !== 'function') {
15
+ throw new ConfigError('OPENAPI_AUTH_MODULE factory must return an object with canProvide and getCredential');
16
+ }
17
+ return provider;
18
+ }
19
+ /**
20
+ * Places a credential where its security scheme says: a header, the query string, a cookie or Authorization.
21
+ */
22
+ function applyScheme(name, scheme, value, headers, url) {
23
+ switch (scheme.type) {
24
+ case 'apiKey': {
25
+ if (!scheme.name)
26
+ throw new Error(`security scheme "${name}" has no parameter name`);
27
+ if (scheme.in === 'header')
28
+ headers[scheme.name] = value;
29
+ else if (scheme.in === 'query')
30
+ url.searchParams.set(scheme.name, value);
31
+ else if (scheme.in === 'cookie')
32
+ headers.Cookie = [headers.Cookie, `${scheme.name}=${encodeURIComponent(value)}`].filter(Boolean).join('; ');
33
+ else
34
+ throw new Error(`security scheme "${name}" uses an unsupported location "${scheme.in}"`);
35
+ return;
36
+ }
37
+ case 'http': {
38
+ const kind = (scheme.scheme ?? '').toLowerCase();
39
+ if (kind === 'bearer')
40
+ headers.Authorization = `Bearer ${value}`;
41
+ else if (kind === 'basic')
42
+ headers.Authorization = `Basic ${value.includes(':') ? Buffer.from(value).toString('base64') : value}`;
43
+ else
44
+ throw new Error(`security scheme "${name}" uses an unsupported HTTP scheme "${scheme.scheme}"`);
45
+ return;
46
+ }
47
+ case 'oauth2':
48
+ case 'openIdConnect':
49
+ headers.Authorization = `Bearer ${value}`;
50
+ return;
51
+ default:
52
+ throw new Error(`security scheme "${name}" has an unsupported type "${scheme.type}"`);
53
+ }
54
+ }
55
+ /**
56
+ * Maps credentials from the environment and an auth provider onto the security schemes of the spec.
57
+ */
58
+ export class Credentials {
59
+ config;
60
+ provider;
61
+ constructor(config, provider) {
62
+ this.config = config;
63
+ this.provider = provider;
64
+ }
65
+ /**
66
+ * Where the credential of a scheme comes from, or null when nothing supplies it.
67
+ */
68
+ source(scheme, context = {}) {
69
+ if (this.config.schemeCredentials.has(schemeEnvSuffix(scheme)))
70
+ return 'env';
71
+ return this.provider?.canProvide(scheme, context) ? 'module' : null;
72
+ }
73
+ /**
74
+ * Credential status of a scheme for spec info.
75
+ */
76
+ describe(scheme) {
77
+ const source = this.source(scheme);
78
+ if (source)
79
+ return source;
80
+ // canProvide does no I/O by contract, so probing with a placeholder identity is safe and tells whether passing one would help.
81
+ return this.provider?.canProvide(scheme, { identity: 'identity' }) ? 'module, when an identity is passed' : 'not configured';
82
+ }
83
+ /**
84
+ * Picks the security alternative for a call: a forced scheme, the first alternative with all credentials, or anonymous.
85
+ */
86
+ select(op, as, context, schemes) {
87
+ if (as === 'anonymous')
88
+ return { mode: 'anonymous' };
89
+ if (as !== 'auto') {
90
+ if (!schemes[as]) {
91
+ const known = Object.keys(schemes);
92
+ throw new Error(`unknown security scheme "${as}"; the spec defines: ${known.length ? known.join(', ') : 'none'}`);
93
+ }
94
+ if (!this.source(as, context))
95
+ throw new Error(`no credential for "${as}": ${this.hint(as)}`);
96
+ return { mode: 'credentials', schemes: [as] };
97
+ }
98
+ for (const alternative of op.security) {
99
+ if (alternative.every((scheme) => this.source(scheme, context))) {
100
+ return alternative.length ? { mode: 'credentials', schemes: alternative } : { mode: 'anonymous' };
101
+ }
102
+ }
103
+ if (op.security.length === 0)
104
+ return { mode: 'anonymous' };
105
+ const missing = [...new Set(op.security.flat())];
106
+ const hints = missing.map((scheme) => this.hint(scheme)).join('; ');
107
+ // Many GET endpoints declare auth but also answer anonymously, and a 401 explains itself; writes don't get that benefit of the doubt.
108
+ if (op.method === 'GET')
109
+ return { mode: 'anonymous', note: `no credentials for ${missing.join(' or ')}, called anonymously — ${hints}` };
110
+ throw new Error(`${op.key} requires ${op.security.map((a) => a.join(' + ')).join(' or ')}: ${hints}`);
111
+ }
112
+ /**
113
+ * Puts the credentials of the selected schemes into the request; reports whether any came from the provider.
114
+ */
115
+ async apply(selection, schemes, context, headers, url) {
116
+ if (selection.mode === 'anonymous')
117
+ return { fromProvider: false };
118
+ let fromProvider = false;
119
+ for (const name of selection.schemes) {
120
+ const scheme = schemes[name];
121
+ if (!scheme)
122
+ throw new Error(`the spec has no security scheme "${name}"`);
123
+ let value = this.config.schemeCredentials.get(schemeEnvSuffix(name));
124
+ if (value === undefined) {
125
+ if (!this.provider)
126
+ throw new Error(`no credential for "${name}": ${this.hint(name)}`);
127
+ value = await this.provider.getCredential(name, context);
128
+ fromProvider = true;
129
+ }
130
+ applyScheme(name, scheme, value, headers, url);
131
+ }
132
+ return { fromProvider };
133
+ }
134
+ /**
135
+ * Tells the user how to supply a credential for a scheme.
136
+ */
137
+ hint(scheme) {
138
+ return `set ${schemeEnvName(scheme)}${this.provider ? ' or pass an identity the auth module accepts' : ''}`;
139
+ }
140
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Runtime configuration read from environment variables.
3
+ */
4
+ export interface ExplorerConfig {
5
+ /** URL of the spec, or an absolute path to a local spec file. */
6
+ specSource: string;
7
+ /** Whether specSource is an http(s) URL. */
8
+ specIsUrl: boolean;
9
+ /** Explicit base URL for calls, without a trailing slash. */
10
+ baseUrl?: string;
11
+ /** Directory for the spec cache and generated types. */
12
+ cacheDir: string;
13
+ /** How often a URL spec is revalidated, in milliseconds. */
14
+ specTtlMs: number;
15
+ /** Timeout of spec fetches and API calls, in milliseconds. */
16
+ timeoutMs: number;
17
+ /** Cap on the size of a tool response, in characters. */
18
+ maxResponseChars: number;
19
+ /** Whether api_request is registered. */
20
+ allowWrite: boolean;
21
+ /** Server name reported to MCP clients. */
22
+ serverName: string;
23
+ /** JSON file with danger overrides. */
24
+ dangerFile?: string;
25
+ /** Directory of markdown recipes; the recipe tool exists only when it is set. */
26
+ recipesDir?: string;
27
+ /** Markdown appended to the server instructions. */
28
+ instructionsFile?: string;
29
+ /** JSONL journal of api_request calls. */
30
+ callLog: string;
31
+ /** ES module whose default export creates an auth provider. */
32
+ authModule?: string;
33
+ /** Headers sent with every call, from OPENAPI_HEADER_<NAME>. */
34
+ staticHeaders: Record<string, string>;
35
+ /** Credentials keyed by the normalized scheme name, from OPENAPI_AUTH_<SCHEME>. */
36
+ schemeCredentials: Map<string, string>;
37
+ }
38
+ /**
39
+ * The environment is missing a required value or holds an invalid one.
40
+ */
41
+ export declare class ConfigError extends Error {
42
+ name: string;
43
+ }
44
+ /**
45
+ * Normalizes a security scheme name into its environment variable suffix: x-admin-token becomes X_ADMIN_TOKEN.
46
+ */
47
+ export declare function schemeEnvSuffix(scheme: string): string;
48
+ /**
49
+ * Name of the environment variable that holds the credential of a scheme.
50
+ */
51
+ export declare function schemeEnvName(scheme: string): string;
52
+ /**
53
+ * Parses KEY=VALUE lines of an env file: comments, `export` prefixes and quoted values are supported.
54
+ */
55
+ export declare function parseEnvText(text: string): Record<string, string>;
56
+ /**
57
+ * Reads and validates the configuration from environment variables.
58
+ */
59
+ export declare function readConfig(env?: NodeJS.ProcessEnv): ExplorerConfig;
package/dist/config.js ADDED
@@ -0,0 +1,164 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+ /**
6
+ * The environment is missing a required value or holds an invalid one.
7
+ */
8
+ export class ConfigError extends Error {
9
+ name = 'ConfigError';
10
+ }
11
+ const AUTH_PREFIX = 'OPENAPI_AUTH_';
12
+ const HEADER_PREFIX = 'OPENAPI_HEADER_';
13
+ const RESERVED_AUTH_KEYS = new Set(['OPENAPI_AUTH_MODULE']);
14
+ /**
15
+ * Normalizes a security scheme name into its environment variable suffix: x-admin-token becomes X_ADMIN_TOKEN.
16
+ */
17
+ export function schemeEnvSuffix(scheme) {
18
+ return scheme
19
+ .toUpperCase()
20
+ .replace(/[^A-Z0-9]+/g, '_')
21
+ .replace(/^_+|_+$/g, '');
22
+ }
23
+ /**
24
+ * Name of the environment variable that holds the credential of a scheme.
25
+ */
26
+ export function schemeEnvName(scheme) {
27
+ return `${AUTH_PREFIX}${schemeEnvSuffix(scheme)}`;
28
+ }
29
+ /**
30
+ * Expands a leading ~ to the home directory.
31
+ */
32
+ function expandHome(value) {
33
+ if (value === '~')
34
+ return homedir();
35
+ return value.startsWith('~/') ? path.join(homedir(), value.slice(2)) : value;
36
+ }
37
+ /**
38
+ * Parses KEY=VALUE lines of an env file: comments, `export` prefixes and quoted values are supported.
39
+ */
40
+ export function parseEnvText(text) {
41
+ const out = {};
42
+ for (const rawLine of text.split(/\r?\n/)) {
43
+ const line = rawLine.trim();
44
+ if (!line || line.startsWith('#'))
45
+ continue;
46
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
47
+ if (!match)
48
+ continue;
49
+ let value = match[2];
50
+ const quote = value[0];
51
+ if ((quote === '"' || quote === "'") && value.endsWith(quote) && value.length >= 2) {
52
+ value = value.slice(1, -1);
53
+ }
54
+ else {
55
+ value = value.replace(/\s+#.*$/, '').trim();
56
+ }
57
+ out[match[1]] = value;
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * Merges an env file into the environment; variables that are already set win, so a client config can override the file.
63
+ */
64
+ function loadEnvFile(file, env) {
65
+ if (!existsSync(file))
66
+ throw new ConfigError(`OPENAPI_ENV_FILE points to a missing file: ${file}`);
67
+ for (const [key, value] of Object.entries(parseEnvText(readFileSync(file, 'utf8')))) {
68
+ if (env[key] === undefined)
69
+ env[key] = value;
70
+ }
71
+ }
72
+ /**
73
+ * Parses an optional positive number, failing loudly instead of silently turning garbage into NaN.
74
+ */
75
+ function positiveNumber(raw, fallback, name) {
76
+ if (raw === undefined || raw.trim() === '')
77
+ return fallback;
78
+ const value = Number(raw);
79
+ if (!Number.isFinite(value) || value <= 0)
80
+ throw new ConfigError(`${name} must be a positive number, got "${raw}"`);
81
+ return value;
82
+ }
83
+ /**
84
+ * Resolves an optional path variable; a variable that is set must point to something that exists.
85
+ */
86
+ function optionalPath(env, name) {
87
+ const raw = env[name]?.trim();
88
+ if (!raw)
89
+ return undefined;
90
+ const resolved = path.resolve(expandHome(raw));
91
+ if (!existsSync(resolved))
92
+ throw new ConfigError(`${name} points to a missing path: ${resolved}`);
93
+ return resolved;
94
+ }
95
+ /**
96
+ * Reads and validates the configuration from environment variables.
97
+ */
98
+ export function readConfig(env = process.env) {
99
+ const envFile = env.OPENAPI_ENV_FILE?.trim();
100
+ if (envFile)
101
+ loadEnvFile(path.resolve(expandHome(envFile)), env);
102
+ const rawSpec = env.OPENAPI_SPEC_URL?.trim();
103
+ if (!rawSpec)
104
+ throw new ConfigError('OPENAPI_SPEC_URL is required: the URL or file path of an OpenAPI 3 JSON spec');
105
+ const specIsUrl = /^https?:\/\//i.test(rawSpec);
106
+ const specSource = specIsUrl ? rawSpec : path.resolve(expandHome(rawSpec));
107
+ if (!specIsUrl && !existsSync(specSource))
108
+ throw new ConfigError(`OPENAPI_SPEC_URL points to a missing file: ${specSource}`);
109
+ let baseUrl;
110
+ const rawBase = env.OPENAPI_BASE_URL?.trim();
111
+ if (rawBase) {
112
+ let parsed;
113
+ try {
114
+ parsed = new URL(rawBase);
115
+ }
116
+ catch {
117
+ throw new ConfigError(`OPENAPI_BASE_URL is not a valid URL: ${rawBase}`);
118
+ }
119
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
120
+ throw new ConfigError(`OPENAPI_BASE_URL must be an http(s) URL, got ${parsed.protocol}`);
121
+ }
122
+ baseUrl = rawBase.replace(/\/+$/, '');
123
+ }
124
+ const staticHeaders = {};
125
+ const schemeCredentials = new Map();
126
+ for (const [key, value] of Object.entries(env)) {
127
+ // An empty value, e.g. from ${VAR:-} in a client config, means "not configured".
128
+ if (!value)
129
+ continue;
130
+ if (key.startsWith(HEADER_PREFIX)) {
131
+ staticHeaders[key.slice(HEADER_PREFIX.length).toLowerCase().replace(/_/g, '-')] = value;
132
+ }
133
+ else if (key.startsWith(AUTH_PREFIX) && !RESERVED_AUTH_KEYS.has(key)) {
134
+ schemeCredentials.set(key.slice(AUTH_PREFIX.length), value);
135
+ }
136
+ }
137
+ const authModule = optionalPath(env, 'OPENAPI_AUTH_MODULE');
138
+ if (!baseUrl && (schemeCredentials.size > 0 || Object.keys(staticHeaders).length > 0 || authModule)) {
139
+ throw new ConfigError('OPENAPI_BASE_URL is required when credentials or headers are configured: they only go to an origin you set explicitly, never to one taken from the spec');
140
+ }
141
+ const rawCacheDir = env.OPENAPI_CACHE_DIR?.trim();
142
+ const cacheDir = rawCacheDir
143
+ ? path.resolve(expandHome(rawCacheDir))
144
+ : path.join(homedir(), '.cache', 'openapi-explorer-mcp', createHash('sha1').update(specSource).digest('hex').slice(0, 12));
145
+ const rawCallLog = env.OPENAPI_CALL_LOG?.trim();
146
+ return {
147
+ specSource,
148
+ specIsUrl,
149
+ baseUrl,
150
+ cacheDir,
151
+ specTtlMs: positiveNumber(env.OPENAPI_SPEC_TTL_S, 900, 'OPENAPI_SPEC_TTL_S') * 1000,
152
+ timeoutMs: positiveNumber(env.OPENAPI_TIMEOUT_MS, 20_000, 'OPENAPI_TIMEOUT_MS'),
153
+ maxResponseChars: positiveNumber(env.OPENAPI_MAX_RESPONSE_CHARS, 40_000, 'OPENAPI_MAX_RESPONSE_CHARS'),
154
+ allowWrite: /^(1|true|yes)$/i.test(env.OPENAPI_ALLOW_WRITE ?? ''),
155
+ serverName: env.OPENAPI_SERVER_NAME?.trim() || 'openapi',
156
+ dangerFile: optionalPath(env, 'OPENAPI_DANGER_FILE'),
157
+ recipesDir: optionalPath(env, 'OPENAPI_RECIPES_DIR'),
158
+ instructionsFile: optionalPath(env, 'OPENAPI_INSTRUCTIONS_FILE'),
159
+ callLog: rawCallLog ? path.resolve(expandHome(rawCallLog)) : path.join(cacheDir, 'calls.jsonl'),
160
+ authModule,
161
+ staticHeaders,
162
+ schemeCredentials,
163
+ };
164
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { HttpMethod } from './spec-index.js';
2
+ /**
3
+ * Outcome of an HTTP call.
4
+ */
5
+ export interface CallResult {
6
+ /** HTTP status. */
7
+ status: number;
8
+ /** Whether the status is 2xx. */
9
+ ok: boolean;
10
+ /** Call duration. */
11
+ durationMs: number;
12
+ /** Rate limit headers the API returned. */
13
+ rateLimit?: Record<string, string>;
14
+ /** Parsed JSON body, raw text, or null for an empty body. */
15
+ body: unknown;
16
+ }
17
+ /**
18
+ * Fills path parameters into a path template and appends the query string.
19
+ */
20
+ export declare function buildUrl(baseUrl: string, pathTemplate: string, pathParams: Record<string, string | number>, query: Record<string, string | number | boolean>): URL;
21
+ /**
22
+ * Sends a request and returns a structured result; authorization and retries are the caller's job.
23
+ */
24
+ export declare function send(method: HttpMethod, url: URL, headers: Record<string, string>, body: unknown, timeoutMs: number): Promise<CallResult>;