openapi-explorer-mcp 0.0.1 → 0.1.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 CHANGED
@@ -19,7 +19,7 @@ the spec and one generic caller.
19
19
  | `api_get` | Calls a GET endpoint. |
20
20
  | `api_request` | Calls an endpoint with any method. Registered only with `OPENAPI_ALLOW_WRITE`; destructive endpoints need `confirm_danger: true`. |
21
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. |
22
+ | `api_credentials` | Keeps credentials for the session or forgets them; shows where the credential of each scheme comes from, never the value. |
23
23
  | `recipe` | Markdown recipes for this API. Registered only with `OPENAPI_RECIPES_DIR`. |
24
24
 
25
25
  ## Configuration
@@ -27,10 +27,9 @@ the spec and one generic caller.
27
27
  | Variable | |
28
28
  |---|---|
29
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. |
30
+ | `OPENAPI_BASE_URL` | Base URL for calls. Required when a credential or header is configured in the environment; otherwise `servers[0].url` of the spec is used. |
31
+ | `OPENAPI_AUTH_<SCHEME>` | A permanent credential for a security scheme — see [Authentication](#authentication). |
32
+ | `OPENAPI_HEADER_<NAME>` | A header sent with every call, whatever the spec says. `OPENAPI_HEADER_X_API_KEY` sends `x-api-key`. |
34
33
  | `OPENAPI_ENV_FILE` | Env file merged into the environment at startup; variables already set win. |
35
34
  | `OPENAPI_ALLOW_WRITE` | `1`, `true` or `yes` registers `api_request`. Off by default. |
36
35
  | `OPENAPI_DANGER_FILE` | JSON with danger overrides — see [Danger rules](#danger-rules). |
@@ -63,11 +62,34 @@ the spec and one generic caller.
63
62
  ## Authentication
64
63
 
65
64
  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.
65
+ goes, and each operation's `security` says which schemes it accepts. A scheme takes its value from the first of
66
+ three places that has one:
67
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:
68
+ | Source | Lives | How |
69
+ |---|---|---|
70
+ | the call | one call | `credentials` argument of `api_get` and `api_request` |
71
+ | the session | until the server restarts | `api_credentials` with `set` and `clear` |
72
+ | the environment | as long as the configuration | `OPENAPI_AUTH_<SCHEME>` |
73
+
74
+ **Permanent credentials** — a static admin token, a service API key — belong in the environment: they stay out of
75
+ the conversation. The variable name is the scheme name upper-cased with every other character replaced by `_`:
76
+ `x-admin-token` → `OPENAPI_AUTH_X_ADMIN_TOKEN`, `bearer` → `OPENAPI_AUTH_BEARER`. A header every call must carry,
77
+ whatever the spec says, goes to `OPENAPI_HEADER_<NAME>`.
78
+
79
+ **Credentials the model supplies** — a token it just obtained, a key the user pasted into the chat — go through
80
+ `credentials` or `api_credentials`. Keys are scheme names as `api_spec_info` lists them; an `apiKey` scheme also
81
+ accepts its header, query or cookie name, so `X-Api-Key` finds a scheme named `ApiKeyAuth`. When the spec declares
82
+ no security schemes at all, the keys are sent as plain headers.
83
+
84
+ ```
85
+ api_request(method: "POST", endpoint: "POST /auth/login", body: { … })
86
+ api_credentials(set: { "bearer": "<accessToken from the response>" })
87
+ api_get(endpoint: "GET /me")
88
+ ```
89
+
90
+ When a call with a credential gets `401`, the response says so in `note.auth`: obtain a fresh value and set it again.
91
+
92
+ **Placement.** Whatever the source, the value goes where the scheme says:
71
93
 
72
94
  | Scheme | Placement |
73
95
  |---|---|
@@ -76,38 +98,20 @@ value is placed where the scheme says:
76
98
  | `http` `basic` | `Authorization: Basic …` — give `user:password` or an already encoded value |
77
99
 
78
100
  **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
+ takes the first alternative whose schemes all have credentials, from any source. `as` can also name a scheme to
102
+ force it, or be `"anonymous"`. When nothing is available, a GET is sent anonymously with a note (many GET endpoints
103
+ declare auth but also answer without it); any other method fails and names every way to supply the credential.
101
104
 
102
105
  **What keeps credentials safe**
103
106
 
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.
107
+ - Credentials go only to the base URL. The origin of every request is checked against it before sending, and path
108
+ parameters are URL-encoded, so a path can't redirect a request elsewhere.
109
+ - Credentials from the environment need `OPENAPI_BASE_URL` set explicitly: a server configuration is long-lived and
110
+ often shared, while the spec's `servers` comes over the network. Credentials the model supplies go to the base URL
111
+ that `api_spec_info` and `api_credentials` report `OPENAPI_BASE_URL`, or the first server of the spec.
112
+ - Tool output and the call journal never contain values: they name the scheme and where its credential came from,
113
+ e.g. `bearer (session)`. A value the model supplies is part of the conversation by nature — keep secrets that
114
+ must not be there in the environment.
111
115
 
112
116
  ## Danger rules
113
117
 
@@ -130,10 +134,15 @@ Every non-GET operation is `write`, and `destructive` when it is a `DELETE` or i
130
134
  npm install
131
135
  npm run typecheck
132
136
  npm run build # tsc into dist/
133
- npm run smoke # stdio checks against scripts/fixtures/pets.json, no network
137
+ npm run smoke # stdio checks against scripts/fixtures/pets.json and a local HTTP server, no internet
134
138
  npm run check # all three
139
+ npm run smoke:package # packs the tarball, installs it in a clean directory and runs the smoke there
135
140
  ```
136
141
 
142
+ `npm publish` runs `check` and `smoke:package` first. The package depends on TypeScript 5.9 directly: the type
143
+ generator declares TypeScript as a peer dependency, and without the pin npm installs TypeScript 7, whose JavaScript
144
+ API the generator can't use.
145
+
137
146
  ## License
138
147
 
139
148
  MIT — see [LICENSE](LICENSE).
package/dist/auth.d.ts CHANGED
@@ -1,51 +1,16 @@
1
1
  import { type ExplorerConfig } from './config.js';
2
2
  import type { Operation, SecurityScheme } from './spec-index.js';
3
+ /** Where a credential used by a call came from. */
4
+ export type CredentialSource = 'call' | 'session' | 'env';
3
5
  /**
4
- * Per-call context passed to an auth provider.
6
+ * Credentials the model supplied, resolved against the spec.
5
7
  */
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;
8
+ export interface SuppliedCredentials {
9
+ /** Values keyed by security scheme name. */
10
+ schemes: Map<string, string>;
11
+ /** Plain headers keyed by lower-cased name; only for specs that declare no security schemes. */
12
+ headers: Map<string, string>;
11
13
  }
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
14
  /** Which security alternative a call uses. */
50
15
  export type AuthSelection = {
51
16
  mode: 'anonymous';
@@ -55,36 +20,55 @@ export type AuthSelection = {
55
20
  schemes: string[];
56
21
  };
57
22
  /**
58
- * Imports OPENAPI_AUTH_MODULE and creates its provider.
23
+ * An empty set of supplied credentials.
59
24
  */
60
- export declare function loadAuthProvider(config: ExplorerConfig): Promise<AuthProvider | undefined>;
25
+ export declare function emptyCredentials(): SuppliedCredentials;
61
26
  /**
62
- * Maps credentials from the environment and an auth provider onto the security schemes of the spec.
27
+ * Where a scheme puts its value, for humans: "header x-api-key" or "Authorization: Bearer".
28
+ */
29
+ export declare function placement(scheme: SecurityScheme): string;
30
+ /**
31
+ * Maps the keys of supplied credentials onto the spec; for a spec without security schemes they are plain headers.
32
+ */
33
+ export declare function resolveSupplied(input: Record<string, string>, schemes: Record<string, SecurityScheme>): SuppliedCredentials;
34
+ /**
35
+ * Maps credentials onto the security schemes of the spec. A value passed in a call wins over one kept for the
36
+ * session, which wins over the environment.
63
37
  */
64
38
  export declare class Credentials {
65
39
  private readonly config;
66
- private readonly provider?;
67
- constructor(config: ExplorerConfig, provider?: AuthProvider | undefined);
40
+ private readonly session;
41
+ constructor(config: ExplorerConfig);
42
+ /**
43
+ * Keeps supplied credentials in memory for the rest of the server session.
44
+ */
45
+ remember(supplied: SuppliedCredentials): void;
46
+ /**
47
+ * Forgets session credentials by key, or all of them for "*"; returns the names that were kept before.
48
+ */
49
+ forget(keys: string[], schemes: Record<string, SecurityScheme>): string[];
68
50
  /**
69
51
  * Where the credential of a scheme comes from, or null when nothing supplies it.
70
52
  */
71
- source(scheme: string, context?: AuthContext): 'env' | 'module' | null;
53
+ source(scheme: string, call?: SuppliedCredentials): CredentialSource | null;
72
54
  /**
73
- * Credential status of a scheme for spec info.
55
+ * Credential status of a scheme outside a call: its source, or "not configured".
74
56
  */
75
57
  describe(scheme: string): string;
58
+ /**
59
+ * Names of plain headers kept for the session.
60
+ */
61
+ sessionHeaders(): string[];
76
62
  /**
77
63
  * Picks the security alternative for a call: a forced scheme, the first alternative with all credentials, or anonymous.
78
64
  */
79
- select(op: Operation, as: string, context: AuthContext, schemes: Record<string, SecurityScheme>): AuthSelection;
65
+ select(op: Operation, as: string, call: SuppliedCredentials, schemes: Record<string, SecurityScheme>): AuthSelection;
80
66
  /**
81
- * Puts the credentials of the selected schemes into the request; reports whether any came from the provider.
67
+ * Puts the selected credentials and plain headers into the request; returns what was used, without values.
82
68
  */
83
- apply(selection: AuthSelection, schemes: Record<string, SecurityScheme>, context: AuthContext, headers: Record<string, string>, url: URL): Promise<{
84
- fromProvider: boolean;
85
- }>;
69
+ apply(selection: AuthSelection, schemes: Record<string, SecurityScheme>, call: SuppliedCredentials, headers: Record<string, string>, url: URL): string;
86
70
  /**
87
- * Tells the user how to supply a credential for a scheme.
71
+ * Tells how to supply a credential for a scheme.
88
72
  */
89
73
  private hint;
90
74
  }
package/dist/auth.js CHANGED
@@ -1,20 +1,67 @@
1
- import { pathToFileURL } from 'node:url';
2
- import { ConfigError, schemeEnvName, schemeEnvSuffix } from './config.js';
1
+ import { schemeEnvName, schemeEnvSuffix } from './config.js';
2
+ const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
3
3
  /**
4
- * Imports OPENAPI_AUTH_MODULE and creates its provider.
4
+ * An empty set of supplied credentials.
5
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');
6
+ export function emptyCredentials() {
7
+ return { schemes: new Map(), headers: new Map() };
8
+ }
9
+ /**
10
+ * Where a scheme puts its value, for humans: "header x-api-key" or "Authorization: Bearer".
11
+ */
12
+ export function placement(scheme) {
13
+ if (scheme.type === 'apiKey')
14
+ return `${scheme.in ?? '?'} ${scheme.name ?? '?'}`;
15
+ if (scheme.type !== 'http')
16
+ return 'Authorization: Bearer';
17
+ const kind = (scheme.scheme ?? '').toLowerCase();
18
+ if (kind === 'bearer')
19
+ return 'Authorization: Bearer';
20
+ if (kind === 'basic')
21
+ return 'Authorization: Basic';
22
+ return `Authorization: ${scheme.scheme ?? '?'}`;
23
+ }
24
+ /**
25
+ * Lists the security schemes of a spec with where each one puts its value.
26
+ */
27
+ function describeSchemes(schemes) {
28
+ const entries = Object.entries(schemes);
29
+ return entries.length ? entries.map(([name, scheme]) => `${name} (${placement(scheme)})`).join(', ') : 'none';
30
+ }
31
+ /**
32
+ * Finds the scheme a key names: the scheme name itself, or the header, query or cookie name of an apiKey scheme.
33
+ */
34
+ function resolveSchemeName(key, schemes) {
35
+ if (schemes[key])
36
+ return key;
37
+ const lower = key.toLowerCase();
38
+ const matches = Object.entries(schemes)
39
+ .filter(([name, scheme]) => name.toLowerCase() === lower || (scheme.type === 'apiKey' && scheme.name?.toLowerCase() === lower))
40
+ .map(([name]) => name);
41
+ if (matches.length === 1)
42
+ return matches[0];
43
+ if (matches.length > 1)
44
+ throw new Error(`"${key}" matches several security schemes (${matches.join(', ')}); use the scheme name`);
45
+ throw new Error(`unknown credential "${key}"; the spec defines: ${describeSchemes(schemes)}`);
46
+ }
47
+ /**
48
+ * Maps the keys of supplied credentials onto the spec; for a spec without security schemes they are plain headers.
49
+ */
50
+ export function resolveSupplied(input, schemes) {
51
+ const out = emptyCredentials();
52
+ const hasSchemes = Object.keys(schemes).length > 0;
53
+ for (const [key, value] of Object.entries(input)) {
54
+ if (/[\r\n]/.test(value))
55
+ throw new Error(`the value of "${key}" contains a line break`);
56
+ if (hasSchemes) {
57
+ out.schemes.set(resolveSchemeName(key, schemes), value);
58
+ continue;
59
+ }
60
+ if (!HEADER_NAME.test(key))
61
+ throw new Error(`"${key}" is not a valid header name; the spec declares no security schemes, so keys are sent as headers`);
62
+ out.headers.set(key.toLowerCase(), value);
16
63
  }
17
- return provider;
64
+ return out;
18
65
  }
19
66
  /**
20
67
  * Places a credential where its security scheme says: a header, the query string, a cookie or Authorization.
@@ -25,11 +72,11 @@ function applyScheme(name, scheme, value, headers, url) {
25
72
  if (!scheme.name)
26
73
  throw new Error(`security scheme "${name}" has no parameter name`);
27
74
  if (scheme.in === 'header')
28
- headers[scheme.name] = value;
75
+ headers[scheme.name.toLowerCase()] = value;
29
76
  else if (scheme.in === 'query')
30
77
  url.searchParams.set(scheme.name, value);
31
78
  else if (scheme.in === 'cookie')
32
- headers.Cookie = [headers.Cookie, `${scheme.name}=${encodeURIComponent(value)}`].filter(Boolean).join('; ');
79
+ headers.cookie = [headers.cookie, `${scheme.name}=${encodeURIComponent(value)}`].filter(Boolean).join('; ');
33
80
  else
34
81
  throw new Error(`security scheme "${name}" uses an unsupported location "${scheme.in}"`);
35
82
  return;
@@ -37,66 +84,97 @@ function applyScheme(name, scheme, value, headers, url) {
37
84
  case 'http': {
38
85
  const kind = (scheme.scheme ?? '').toLowerCase();
39
86
  if (kind === 'bearer')
40
- headers.Authorization = `Bearer ${value}`;
87
+ headers.authorization = `Bearer ${value}`;
41
88
  else if (kind === 'basic')
42
- headers.Authorization = `Basic ${value.includes(':') ? Buffer.from(value).toString('base64') : value}`;
89
+ headers.authorization = `Basic ${value.includes(':') ? Buffer.from(value).toString('base64') : value}`;
43
90
  else
44
91
  throw new Error(`security scheme "${name}" uses an unsupported HTTP scheme "${scheme.scheme}"`);
45
92
  return;
46
93
  }
47
94
  case 'oauth2':
48
95
  case 'openIdConnect':
49
- headers.Authorization = `Bearer ${value}`;
96
+ headers.authorization = `Bearer ${value}`;
50
97
  return;
51
98
  default:
52
99
  throw new Error(`security scheme "${name}" has an unsupported type "${scheme.type}"`);
53
100
  }
54
101
  }
55
102
  /**
56
- * Maps credentials from the environment and an auth provider onto the security schemes of the spec.
103
+ * Maps credentials onto the security schemes of the spec. A value passed in a call wins over one kept for the
104
+ * session, which wins over the environment.
57
105
  */
58
106
  export class Credentials {
59
107
  config;
60
- provider;
61
- constructor(config, provider) {
108
+ session = emptyCredentials();
109
+ constructor(config) {
62
110
  this.config = config;
63
- this.provider = provider;
111
+ }
112
+ /**
113
+ * Keeps supplied credentials in memory for the rest of the server session.
114
+ */
115
+ remember(supplied) {
116
+ for (const [name, value] of supplied.schemes)
117
+ this.session.schemes.set(name, value);
118
+ for (const [name, value] of supplied.headers)
119
+ this.session.headers.set(name, value);
120
+ }
121
+ /**
122
+ * Forgets session credentials by key, or all of them for "*"; returns the names that were kept before.
123
+ */
124
+ forget(keys, schemes) {
125
+ const kept = [...this.session.schemes.keys(), ...this.session.headers.keys()];
126
+ if (keys.includes('*')) {
127
+ this.session.schemes.clear();
128
+ this.session.headers.clear();
129
+ return kept;
130
+ }
131
+ const hasSchemes = Object.keys(schemes).length > 0;
132
+ const names = keys.map((key) => (hasSchemes ? resolveSchemeName(key, schemes) : key.toLowerCase()));
133
+ for (const name of names) {
134
+ this.session.schemes.delete(name);
135
+ this.session.headers.delete(name);
136
+ }
137
+ return kept.filter((name) => names.includes(name));
64
138
  }
65
139
  /**
66
140
  * Where the credential of a scheme comes from, or null when nothing supplies it.
67
141
  */
68
- source(scheme, context = {}) {
142
+ source(scheme, call = emptyCredentials()) {
143
+ if (call.schemes.has(scheme))
144
+ return 'call';
145
+ if (this.session.schemes.has(scheme))
146
+ return 'session';
69
147
  if (this.config.schemeCredentials.has(schemeEnvSuffix(scheme)))
70
148
  return 'env';
71
- return this.provider?.canProvide(scheme, context) ? 'module' : null;
149
+ return null;
72
150
  }
73
151
  /**
74
- * Credential status of a scheme for spec info.
152
+ * Credential status of a scheme outside a call: its source, or "not configured".
75
153
  */
76
154
  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';
155
+ return this.source(scheme) ?? 'not configured';
156
+ }
157
+ /**
158
+ * Names of plain headers kept for the session.
159
+ */
160
+ sessionHeaders() {
161
+ return [...this.session.headers.keys()];
82
162
  }
83
163
  /**
84
164
  * Picks the security alternative for a call: a forced scheme, the first alternative with all credentials, or anonymous.
85
165
  */
86
- select(op, as, context, schemes) {
166
+ select(op, as, call, schemes) {
87
167
  if (as === 'anonymous')
88
168
  return { mode: 'anonymous' };
89
169
  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))
170
+ if (!schemes[as])
171
+ throw new Error(`unknown security scheme "${as}"; the spec defines: ${describeSchemes(schemes)}`);
172
+ if (!this.source(as, call))
95
173
  throw new Error(`no credential for "${as}": ${this.hint(as)}`);
96
174
  return { mode: 'credentials', schemes: [as] };
97
175
  }
98
176
  for (const alternative of op.security) {
99
- if (alternative.every((scheme) => this.source(scheme, context))) {
177
+ if (alternative.every((scheme) => this.source(scheme, call))) {
100
178
  return alternative.length ? { mode: 'credentials', schemes: alternative } : { mode: 'anonymous' };
101
179
  }
102
180
  }
@@ -110,31 +188,33 @@ export class Credentials {
110
188
  throw new Error(`${op.key} requires ${op.security.map((a) => a.join(' + ')).join(' or ')}: ${hints}`);
111
189
  }
112
190
  /**
113
- * Puts the credentials of the selected schemes into the request; reports whether any came from the provider.
191
+ * Puts the selected credentials and plain headers into the request; returns what was used, without values.
114
192
  */
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)
193
+ apply(selection, schemes, call, headers, url) {
194
+ const used = [];
195
+ if (selection.mode === 'credentials') {
196
+ for (const name of selection.schemes) {
197
+ const scheme = schemes[name];
198
+ if (!scheme)
199
+ throw new Error(`the spec has no security scheme "${name}"`);
200
+ const source = this.source(name, call);
201
+ const value = call.schemes.get(name) ?? this.session.schemes.get(name) ?? this.config.schemeCredentials.get(schemeEnvSuffix(name));
202
+ if (!source || value === undefined)
126
203
  throw new Error(`no credential for "${name}": ${this.hint(name)}`);
127
- value = await this.provider.getCredential(name, context);
128
- fromProvider = true;
204
+ applyScheme(name, scheme, value, headers, url);
205
+ used.push(`${name} (${source})`);
129
206
  }
130
- applyScheme(name, scheme, value, headers, url);
131
207
  }
132
- return { fromProvider };
208
+ for (const [name, value] of new Map([...this.session.headers, ...call.headers])) {
209
+ headers[name] = value;
210
+ used.push(`header ${name} (${call.headers.has(name) ? 'call' : 'session'})`);
211
+ }
212
+ return used.length ? used.join(' + ') : 'anonymous';
133
213
  }
134
214
  /**
135
- * Tells the user how to supply a credential for a scheme.
215
+ * Tells how to supply a credential for a scheme.
136
216
  */
137
217
  hint(scheme) {
138
- return `set ${schemeEnvName(scheme)}${this.provider ? ' or pass an identity the auth module accepts' : ''}`;
218
+ return `pass credentials: { "${scheme}": "…" }, keep one with api_credentials, or set ${schemeEnvName(scheme)}`;
139
219
  }
140
220
  }
package/dist/config.d.ts CHANGED
@@ -28,8 +28,6 @@ export interface ExplorerConfig {
28
28
  instructionsFile?: string;
29
29
  /** JSONL journal of api_request calls. */
30
30
  callLog: string;
31
- /** ES module whose default export creates an auth provider. */
32
- authModule?: string;
33
31
  /** Headers sent with every call, from OPENAPI_HEADER_<NAME>. */
34
32
  staticHeaders: Record<string, string>;
35
33
  /** Credentials keyed by the normalized scheme name, from OPENAPI_AUTH_<SCHEME>. */
package/dist/config.js CHANGED
@@ -10,7 +10,6 @@ export class ConfigError extends Error {
10
10
  }
11
11
  const AUTH_PREFIX = 'OPENAPI_AUTH_';
12
12
  const HEADER_PREFIX = 'OPENAPI_HEADER_';
13
- const RESERVED_AUTH_KEYS = new Set(['OPENAPI_AUTH_MODULE']);
14
13
  /**
15
14
  * Normalizes a security scheme name into its environment variable suffix: x-admin-token becomes X_ADMIN_TOKEN.
16
15
  */
@@ -130,12 +129,11 @@ export function readConfig(env = process.env) {
130
129
  if (key.startsWith(HEADER_PREFIX)) {
131
130
  staticHeaders[key.slice(HEADER_PREFIX.length).toLowerCase().replace(/_/g, '-')] = value;
132
131
  }
133
- else if (key.startsWith(AUTH_PREFIX) && !RESERVED_AUTH_KEYS.has(key)) {
132
+ else if (key.startsWith(AUTH_PREFIX)) {
134
133
  schemeCredentials.set(key.slice(AUTH_PREFIX.length), value);
135
134
  }
136
135
  }
137
- const authModule = optionalPath(env, 'OPENAPI_AUTH_MODULE');
138
- if (!baseUrl && (schemeCredentials.size > 0 || Object.keys(staticHeaders).length > 0 || authModule)) {
136
+ if (!baseUrl && (schemeCredentials.size > 0 || Object.keys(staticHeaders).length > 0)) {
139
137
  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
138
  }
141
139
  const rawCacheDir = env.OPENAPI_CACHE_DIR?.trim();
@@ -157,7 +155,6 @@ export function readConfig(env = process.env) {
157
155
  recipesDir: optionalPath(env, 'OPENAPI_RECIPES_DIR'),
158
156
  instructionsFile: optionalPath(env, 'OPENAPI_INSTRUCTIONS_FILE'),
159
157
  callLog: rawCallLog ? path.resolve(expandHome(rawCallLog)) : path.join(cacheDir, 'calls.jsonl'),
160
- authModule,
161
158
  staticHeaders,
162
159
  schemeCredentials,
163
160
  };
package/dist/schemas.d.ts CHANGED
@@ -50,7 +50,7 @@ export declare const getInput: {
50
50
  path_params: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
51
51
  query: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
52
52
  as: z.ZodDefault<z.ZodString>;
53
- identity: z.ZodOptional<z.ZodString>;
53
+ credentials: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
54
54
  };
55
55
  export declare const requestInput: {
56
56
  method: z.ZodEnum<{
@@ -65,14 +65,13 @@ export declare const requestInput: {
65
65
  query: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
66
66
  body: z.ZodOptional<z.ZodUnknown>;
67
67
  as: z.ZodDefault<z.ZodString>;
68
- identity: z.ZodOptional<z.ZodString>;
68
+ credentials: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
69
69
  reason: z.ZodOptional<z.ZodString>;
70
70
  confirm_danger: z.ZodDefault<z.ZodBoolean>;
71
71
  };
72
- export declare const authInput: {
73
- identity: z.ZodOptional<z.ZodString>;
74
- refresh: z.ZodDefault<z.ZodBoolean>;
75
- show_token: z.ZodDefault<z.ZodBoolean>;
72
+ export declare const credentialsInput: {
73
+ set: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
74
+ clear: z.ZodDefault<z.ZodArray<z.ZodString>>;
76
75
  };
77
76
  export declare const callLogInput: {
78
77
  limit: z.ZodDefault<z.ZodNumber>;
package/dist/schemas.js CHANGED
@@ -9,8 +9,12 @@ const renderMode = z.enum(['outline', 'json']).default('outline').describe('outl
9
9
  const as = z
10
10
  .string()
11
11
  .default('auto')
12
- .describe("'auto' uses the first security alternative with configured credentials; 'anonymous' sends none; or a security scheme name from the spec");
13
- const identity = z.string().optional().describe('Identity passed to the auth module, e.g. a user id');
12
+ .describe("'auto' uses the first security alternative that has credentials; 'anonymous' sends none; or a security scheme name from the spec");
13
+ const credentialMap = z.record(z.string(), z.string().min(1));
14
+ const credentials = credentialMap
15
+ .optional()
16
+ .describe('Credentials for this call only, keyed by security scheme name from api_spec_info; an apiKey scheme also accepts its header name. ' +
17
+ 'They win over api_credentials and the environment. When the spec declares no security schemes, keys are sent as headers');
14
18
  const pathParams = z.record(z.string(), z.union([z.string(), z.number()])).default({}).describe('Path parameters');
15
19
  const query = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).default({}).describe('Query-string parameters');
16
20
  export const specInfoInput = {
@@ -36,7 +40,7 @@ export const typesInput = {
36
40
  include: z.array(z.enum(['request', 'response', 'params'])).default(['request', 'response', 'params']),
37
41
  name_prefix: z.string().default('').describe('Prefix for generated type names'),
38
42
  };
39
- export const getInput = { endpoint, path_params: pathParams, query, as, identity };
43
+ export const getInput = { endpoint, path_params: pathParams, query, as, credentials };
40
44
  export const requestInput = {
41
45
  method,
42
46
  endpoint,
@@ -44,14 +48,13 @@ export const requestInput = {
44
48
  query,
45
49
  body: z.unknown().optional().describe('JSON body'),
46
50
  as,
47
- identity,
51
+ credentials,
48
52
  reason: z.string().optional().describe('Note for the call journal: why the call was made'),
49
53
  confirm_danger: z.boolean().default(false).describe('Required for operations classified as destructive'),
50
54
  };
51
- export const authInput = {
52
- identity,
53
- refresh: z.boolean().default(false).describe('Mint new tokens even if cached ones are still valid'),
54
- show_token: z.boolean().default(false).describe('Return full tokens instead of previews'),
55
+ export const credentialsInput = {
56
+ set: credentialMap.default({}).describe('Credentials to keep for this server session, keyed like `credentials` in api_get'),
57
+ clear: z.array(z.string()).default([]).describe('Keys to forget; ["*"] forgets everything kept in this session'),
55
58
  };
56
59
  export const callLogInput = {
57
60
  limit: z.number().int().min(1).max(500).default(50),
package/dist/server.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { type AuthProvider } from './auth.js';
2
1
  import { type ExplorerConfig } from './config.js';
3
2
  import { type DangerRules } from './risk.js';
4
3
  /**
@@ -6,11 +5,10 @@ import { type DangerRules } from './risk.js';
6
5
  */
7
6
  export declare class OpenApiExplorerServer {
8
7
  private readonly config;
9
- private readonly provider;
10
8
  private readonly server;
11
9
  private readonly store;
12
10
  private readonly credentials;
13
- constructor(config: ExplorerConfig, rules: DangerRules, provider: AuthProvider | undefined, extraInstructions: string);
11
+ constructor(config: ExplorerConfig, rules: DangerRules, extraInstructions: string);
14
12
  /**
15
13
  * Builds the server from environment variables; exits with a readable message when they are wrong.
16
14
  */
@@ -36,11 +34,19 @@ export declare class OpenApiExplorerServer {
36
34
  */
37
35
  private baseUrl;
38
36
  /**
39
- * Calls an operation: picks credentials, checks the origin, retries once on 401 with fresh provider tokens.
37
+ * Base URL for calls, or null when there is none; for reports that must not fail.
38
+ */
39
+ private baseUrlOrNull;
40
+ /**
41
+ * Security schemes with where each puts its value and where its credential comes from — never the value.
42
+ */
43
+ private schemeStatus;
44
+ /**
45
+ * Calls an operation: resolves and picks credentials, checks the origin, explains a 401 and a 404.
40
46
  */
41
47
  private performCall;
42
48
  /**
43
- * Registers every tool; api_request, api_auth and recipe only when configured.
49
+ * Registers every tool; api_request and recipe only when configured.
44
50
  */
45
51
  private registerTools;
46
52
  }
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import path from 'node:path';
4
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
- import { Credentials, loadAuthProvider } from './auth.js';
6
+ import { Credentials, emptyCredentials, placement, resolveSupplied } from './auth.js';
7
7
  import { readConfig, schemeEnvName } from './config.js';
8
8
  import { buildUrl, send } from './http.js';
9
9
  import { appendJsonl, tailJsonl } from './journal.js';
@@ -24,7 +24,7 @@ const BASE_INSTRUCTIONS = [
24
24
  '',
25
25
  '- Refer to endpoints as "METHOD /path"; operationIds are not always unique.',
26
26
  '- Summaries can be missing or wrong — check the path, method and response shape.',
27
- '- Authentication follows the security schemes of the spec. api_spec_info shows which schemes have credentials; `as` picks one explicitly.',
27
+ '- Authentication follows the security schemes of the spec. api_spec_info shows which schemes have credentials. Pass `credentials` for one call or keep them with api_credentials; `as` picks a scheme explicitly.',
28
28
  '- Destructive endpoints need confirm_danger: true in api_request.',
29
29
  ].join('\n');
30
30
  /**
@@ -32,15 +32,13 @@ const BASE_INSTRUCTIONS = [
32
32
  */
33
33
  export class OpenApiExplorerServer {
34
34
  config;
35
- provider;
36
35
  server;
37
36
  store;
38
37
  credentials;
39
- constructor(config, rules, provider, extraInstructions) {
38
+ constructor(config, rules, extraInstructions) {
40
39
  this.config = config;
41
- this.provider = provider;
42
40
  this.store = new SpecStore(config, rules);
43
- this.credentials = new Credentials(config, provider);
41
+ this.credentials = new Credentials(config);
44
42
  const instructions = extraInstructions ? `${BASE_INSTRUCTIONS}\n\n${extraInstructions}` : BASE_INSTRUCTIONS;
45
43
  this.server = new McpServer({ name: config.serverName, version: VERSION }, { capabilities: { tools: {} }, instructions });
46
44
  this.registerTools();
@@ -52,9 +50,8 @@ export class OpenApiExplorerServer {
52
50
  try {
53
51
  const config = readConfig();
54
52
  const rules = loadDangerRules(config.dangerFile);
55
- const provider = await loadAuthProvider(config);
56
53
  const extra = config.instructionsFile ? readFileSync(config.instructionsFile, 'utf8').trim() : '';
57
- return new OpenApiExplorerServer(config, rules, provider, extra);
54
+ return new OpenApiExplorerServer(config, rules, extra);
58
55
  }
59
56
  catch (error) {
60
57
  // stdout carries JSON-RPC, so startup errors go to stderr.
@@ -119,28 +116,48 @@ export class OpenApiExplorerServer {
119
116
  throw new Error(`the spec server "${first}" is relative and the spec is a local file; set OPENAPI_BASE_URL`);
120
117
  }
121
118
  /**
122
- * Calls an operation: picks credentials, checks the origin, retries once on 401 with fresh provider tokens.
119
+ * Base URL for calls, or null when there is none; for reports that must not fail.
120
+ */
121
+ baseUrlOrNull(state) {
122
+ try {
123
+ return this.baseUrl(state);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Security schemes with where each puts its value and where its credential comes from — never the value.
131
+ */
132
+ schemeStatus(schemes) {
133
+ return Object.entries(schemes).map(([name, scheme]) => ({
134
+ name,
135
+ type: scheme.type,
136
+ placement: placement(scheme),
137
+ credential: this.credentials.describe(name),
138
+ env: schemeEnvName(name),
139
+ }));
140
+ }
141
+ /**
142
+ * Calls an operation: resolves and picks credentials, checks the origin, explains a 401 and a 404.
123
143
  */
124
144
  async performCall(state, op, args) {
125
145
  const base = this.baseUrl(state);
126
146
  const origin = new URL(base).origin;
127
- const context = { identity: args.identity };
128
147
  const schemes = state.index.securitySchemes;
129
- const selection = this.credentials.select(op, args.as, context, schemes);
130
- const attempt = async (force) => {
131
- const url = buildUrl(base, op.path, args.pathParams, args.query);
132
- const headers = { ...this.config.staticHeaders };
133
- const applied = await this.credentials.apply(selection, schemes, { ...context, force }, headers, url);
134
- if (url.origin !== origin)
135
- throw new Error(`refusing to send the request to ${url.origin}; only ${origin} is allowed`);
136
- return { result: await send(args.method, url, headers, args.body, this.config.timeoutMs), applied };
137
- };
138
- let { result, applied } = await attempt(false);
139
- if (result.status === 401 && applied.fromProvider)
140
- ({ result, applied } = await attempt(true));
148
+ const call = args.credentials ? resolveSupplied(args.credentials, schemes) : emptyCredentials();
149
+ const selection = this.credentials.select(op, args.as, call, schemes);
150
+ const url = buildUrl(base, op.path, args.pathParams, args.query);
151
+ const headers = { ...this.config.staticHeaders };
152
+ const auth = this.credentials.apply(selection, schemes, call, headers, url);
153
+ if (url.origin !== origin)
154
+ throw new Error(`refusing to send the request to ${url.origin}; only ${origin} is allowed`);
155
+ const result = await send(args.method, url, headers, args.body, this.config.timeoutMs);
141
156
  const note = {};
142
157
  if (selection.mode === 'anonymous' && selection.note)
143
158
  note.auth = selection.note;
159
+ else if (result.status === 401 && auth !== 'anonymous')
160
+ note.auth = `the API rejected ${auth}: the credential is wrong or expired — supply a fresh one`;
144
161
  if (result.status === 404) {
145
162
  const fresh = await this.store.forceRevalidate().catch(() => null);
146
163
  note.specRecheck = fresh?.index.byKey.has(op.key)
@@ -148,13 +165,13 @@ export class OpenApiExplorerServer {
148
165
  : 'the path is gone from the spec — the API has probably changed';
149
166
  }
150
167
  return {
151
- auth: selection.mode === 'anonymous' ? 'anonymous' : selection.schemes.join(' + '),
168
+ auth,
152
169
  ...result,
153
170
  ...(Object.keys(note).length ? { note } : {}),
154
171
  };
155
172
  }
156
173
  /**
157
- * Registers every tool; api_request, api_auth and recipe only when configured.
174
+ * Registers every tool; api_request and recipe only when configured.
158
175
  */
159
176
  registerTools() {
160
177
  this.server.registerTool('api_spec_info', {
@@ -175,15 +192,7 @@ export class OpenApiExplorerServer {
175
192
  baseUrl: this.config.baseUrl ?? state.index.servers[0] ?? null,
176
193
  counts: state.index.counts,
177
194
  groups,
178
- securitySchemes: Object.entries(state.index.securitySchemes).map(([name, scheme]) => ({
179
- name,
180
- type: scheme.type,
181
- ...(scheme.in ? { in: scheme.in } : {}),
182
- ...(scheme.name ? { parameter: scheme.name } : {}),
183
- ...(scheme.scheme ? { scheme: scheme.scheme } : {}),
184
- credential: this.credentials.describe(name),
185
- env: schemeEnvName(name),
186
- })),
195
+ securitySchemes: this.schemeStatus(state.index.securitySchemes),
187
196
  writes: this.config.allowWrite ? 'enabled' : 'disabled (set OPENAPI_ALLOW_WRITE to register api_request)',
188
197
  ...(state.meta.changed ? { changed: state.meta.changed } : {}),
189
198
  ...(state.offline ? { offline: { since: new Date(state.offline.since).toISOString(), reason: state.offline.reason } } : {}),
@@ -355,12 +364,12 @@ export class OpenApiExplorerServer {
355
364
  description: 'Calls a GET endpoint and returns the response. Read-only: the method is fixed.',
356
365
  inputSchema: schemas.getInput,
357
366
  annotations: { readOnlyHint: true, openWorldHint: true },
358
- }, ({ endpoint, path_params, query, as, identity }) => this.run(async () => {
367
+ }, ({ endpoint, path_params, query, as, credentials }) => this.run(async () => {
359
368
  const state = await this.store.load();
360
369
  const op = resolveEndpoint(state.index, endpoint);
361
370
  if (op.method !== 'GET')
362
371
  throw new Error(`${op.key} is not a GET endpoint${this.config.allowWrite ? '; use api_request' : ''}`);
363
- const response = await this.performCall(state, op, { method: 'GET', pathParams: path_params, query, as, identity });
372
+ const response = await this.performCall(state, op, { method: 'GET', pathParams: path_params, query, as, credentials });
364
373
  return { key: op.key, ...response, ...this.specNote(state) };
365
374
  }));
366
375
  if (this.config.allowWrite) {
@@ -369,7 +378,7 @@ export class OpenApiExplorerServer {
369
378
  description: 'Calls an endpoint with any method, including writes. Destructive endpoints need confirm_danger: true. Calls are recorded in api_call_log.',
370
379
  inputSchema: schemas.requestInput,
371
380
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
372
- }, ({ method, endpoint, path_params, query, body, as, identity, reason, confirm_danger }) => this.run(async () => {
381
+ }, ({ method, endpoint, path_params, query, body, as, credentials, reason, confirm_danger }) => this.run(async () => {
373
382
  const state = await this.store.load();
374
383
  const op = resolveEndpoint(state.index, endpoint);
375
384
  if (method !== op.method)
@@ -377,7 +386,7 @@ export class OpenApiExplorerServer {
377
386
  if (op.danger === 'destructive' && !confirm_danger) {
378
387
  throw new Error(`${op.key} is destructive: ${op.dangerReason}. Repeat with confirm_danger: true if this is intended.`);
379
388
  }
380
- const response = await this.performCall(state, op, { method, pathParams: path_params, query, body, as, identity });
389
+ const response = await this.performCall(state, op, { method, pathParams: path_params, query, body, as, credentials });
381
390
  const responseBody = response.body;
382
391
  appendJsonl(this.config.callLog, {
383
392
  ts: new Date().toISOString(),
@@ -393,25 +402,27 @@ export class OpenApiExplorerServer {
393
402
  return { key: op.key, journaled: true, ...response, ...this.specNote(state) };
394
403
  }));
395
404
  }
396
- const provider = this.provider;
397
- if (provider?.authenticate) {
398
- this.server.registerTool('api_auth', {
399
- title: 'Mint tokens',
400
- description: 'Mints or refreshes tokens through the auth module. show_token returns full tokens instead of previews.',
401
- inputSchema: schemas.authInput,
402
- annotations: { readOnlyHint: true, openWorldHint: true },
403
- }, ({ identity, refresh, show_token }) => this.run(async () => {
404
- const session = await provider.authenticate({ identity, force: refresh });
405
- const preview = (token) => (token ? `${token.slice(0, 12)}…(${token.length})` : undefined);
406
- return {
407
- identity: session.identity,
408
- expiresAt: session.expiresAt,
409
- accessToken: show_token ? session.accessToken : preview(session.accessToken),
410
- refreshToken: show_token ? session.refreshToken : preview(session.refreshToken),
411
- ...(show_token ? {} : { note: 'show_token: true returns the full tokens' }),
412
- };
413
- }));
414
- }
405
+ this.server.registerTool('api_credentials', {
406
+ title: 'Session credentials',
407
+ description: 'Keeps credentials in memory for this server session or forgets them, and shows where the credential of each security scheme comes from — never the values. ' +
408
+ 'Keys are scheme names from api_spec_info; an apiKey scheme also accepts its header name. A credential passed in a call wins over a session one, which wins over the environment.',
409
+ inputSchema: schemas.credentialsInput,
410
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
411
+ }, ({ set, clear }) => this.run(async () => {
412
+ const state = await this.store.load();
413
+ const schemes = state.index.securitySchemes;
414
+ // Resolve first, so a bad key in `set` fails before anything is forgotten.
415
+ const supplied = resolveSupplied(set, schemes);
416
+ const forgotten = clear.length ? this.credentials.forget(clear, schemes) : [];
417
+ this.credentials.remember(supplied);
418
+ const sessionHeaders = this.credentials.sessionHeaders();
419
+ return {
420
+ ...(forgotten.length ? { forgotten } : {}),
421
+ baseUrl: this.baseUrlOrNull(state),
422
+ securitySchemes: this.schemeStatus(schemes),
423
+ ...(sessionHeaders.length ? { sessionHeaders } : {}),
424
+ };
425
+ }));
415
426
  this.server.registerTool('api_call_log', {
416
427
  title: 'Call journal',
417
428
  description: 'What api_request has called: endpoint, status and ids from responses — use it to clean up what was created.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openapi-explorer-mcp",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "MCP server for any OpenAPI 3 spec: search endpoints, inspect request and response shapes, generate TypeScript types, and call endpoints with credentials mapped to the spec's security schemes.",
5
5
  "license": "MIT",
6
6
  "author": "Eugene Trofimov",
@@ -8,13 +8,6 @@
8
8
  "bin": {
9
9
  "openapi-explorer-mcp": "dist/index.js"
10
10
  },
11
- "exports": {
12
- ".": {
13
- "types": "./dist/auth.d.ts",
14
- "default": "./dist/auth.js"
15
- },
16
- "./package.json": "./package.json"
17
- },
18
11
  "files": [
19
12
  "dist",
20
13
  "README.md",
@@ -27,7 +20,9 @@
27
20
  "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
28
21
  "typecheck": "tsc --noEmit",
29
22
  "smoke": "node scripts/smoke.mjs",
23
+ "smoke:package": "node scripts/smoke-package.mjs",
30
24
  "check": "npm run typecheck && npm run build && npm run smoke",
25
+ "prepublishOnly": "npm run check && npm run smoke:package",
31
26
  "prepack": "npm run build"
32
27
  },
33
28
  "keywords": [
@@ -50,10 +45,10 @@
50
45
  "dependencies": {
51
46
  "@hey-api/openapi-ts": "^0.95.0",
52
47
  "@modelcontextprotocol/sdk": "^1.30.0",
48
+ "typescript": "~5.9.3",
53
49
  "zod": "^4.6.5"
54
50
  },
55
51
  "devDependencies": {
56
- "@types/node": "^20.19.43",
57
- "typescript": "^5.9.3"
52
+ "@types/node": "^20.19.43"
58
53
  }
59
54
  }