@mcpmake/core 0.2.4 → 0.2.5
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/dist/analyzer/dom-parser.d.ts +7 -0
- package/dist/analyzer/dom-parser.d.ts.map +1 -1
- package/dist/analyzer/dom-parser.js +31 -5
- package/dist/analyzer/dom-parser.js.map +1 -1
- package/dist/analyzer/site-crawler.d.ts.map +1 -1
- package/dist/analyzer/site-crawler.js +40 -8
- package/dist/analyzer/site-crawler.js.map +1 -1
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js +113 -6
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/project-scaffolder.d.ts +13 -0
- package/dist/emitter/project-scaffolder.d.ts.map +1 -1
- package/dist/emitter/project-scaffolder.js +29 -0
- package/dist/emitter/project-scaffolder.js.map +1 -1
- package/dist/emitter/python-template-loader.d.ts.map +1 -1
- package/dist/emitter/python-template-loader.js +10 -0
- package/dist/emitter/python-template-loader.js.map +1 -1
- package/dist/emitter/python-templates/server.py.hbs +78 -8
- package/dist/emitter/site-templates/browser-manager.ts.hbs +81 -32
- package/dist/emitter/site-templates/server-main-http.ts.hbs +25 -7
- package/dist/emitter/site-templates/tool-handler-form.ts.hbs +10 -8
- package/dist/emitter/template-loader.d.ts.map +1 -1
- package/dist/emitter/template-loader.js +9 -0
- package/dist/emitter/template-loader.js.map +1 -1
- package/dist/emitter/templates/auth-provider.ts.hbs +81 -20
- package/dist/emitter/templates/config.ts.hbs +7 -0
- package/dist/emitter/templates/env.example.hbs +6 -0
- package/dist/emitter/templates/http-executor.ts.hbs +150 -7
- package/dist/emitter/templates/server-main-http.ts.hbs +60 -4
- package/dist/emitter/templates/tool-handler.ts.hbs +37 -2
- package/dist/emitter/templates/tool-test.ts.hbs +9 -1
- package/dist/emitter/worker-templates/config.ts.hbs +7 -0
- package/dist/emitter/worker-templates/tool-handler.ts.hbs +37 -2
- package/dist/emitter/worker-templates/worker.ts.hbs +33 -2
- package/dist/parser/operation-extractor.d.ts.map +1 -1
- package/dist/parser/operation-extractor.js +12 -1
- package/dist/parser/operation-extractor.js.map +1 -1
- package/dist/parser/overlay-loader.d.ts.map +1 -1
- package/dist/parser/overlay-loader.js +11 -2
- package/dist/parser/overlay-loader.js.map +1 -1
- package/dist/parser/schema-converter.d.ts +13 -2
- package/dist/parser/schema-converter.d.ts.map +1 -1
- package/dist/parser/schema-converter.js +30 -19
- package/dist/parser/schema-converter.js.map +1 -1
- package/dist/pricing.d.ts +5 -1
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +7 -3
- package/dist/pricing.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +6 -21
- package/dist/providers/index.js.map +1 -1
- package/dist/recorder/browser-recorder.d.ts.map +1 -1
- package/dist/recorder/browser-recorder.js +39 -7
- package/dist/recorder/browser-recorder.js.map +1 -1
- package/dist/site-transformer/tool-generator.d.ts.map +1 -1
- package/dist/site-transformer/tool-generator.js +90 -22
- package/dist/site-transformer/tool-generator.js.map +1 -1
- package/dist/transformer/auth-detector.d.ts.map +1 -1
- package/dist/transformer/auth-detector.js +11 -4
- package/dist/transformer/auth-detector.js.map +1 -1
- package/dist/transformer/naming.d.ts.map +1 -1
- package/dist/transformer/naming.js +16 -8
- package/dist/transformer/naming.js.map +1 -1
- package/dist/transformer/resource-builder.d.ts.map +1 -1
- package/dist/transformer/resource-builder.js +13 -6
- package/dist/transformer/resource-builder.js.map +1 -1
- package/dist/transformer/stainless-translator.d.ts +6 -0
- package/dist/transformer/stainless-translator.d.ts.map +1 -1
- package/dist/transformer/stainless-translator.js +18 -1
- package/dist/transformer/stainless-translator.js.map +1 -1
- package/dist/transformer/tool-builder.d.ts.map +1 -1
- package/dist/transformer/tool-builder.js +111 -13
- package/dist/transformer/tool-builder.js.map +1 -1
- package/dist/types/index.d.ts +72 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/site.d.ts +19 -1
- package/dist/types/site.d.ts.map +1 -1
- package/dist/utils/sanitize.d.ts +62 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +141 -0
- package/dist/utils/sanitize.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-scaffolder.js","sourceRoot":"","sources":["../../src/emitter/project-scaffolder.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"project-scaffolder.js","sourceRoot":"","sources":["../../src/emitter/project-scaffolder.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/python-template-loader.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"python-template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/python-template-loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAkCH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAUpE"}
|
|
@@ -5,17 +5,27 @@ import { readFileSync } from 'node:fs';
|
|
|
5
5
|
import { resolve, dirname } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import Handlebars from 'handlebars';
|
|
8
|
+
import { escapePyString } from '../utils/sanitize.js';
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const PYTHON_TEMPLATE_DIR = resolve(__dirname, 'python-templates');
|
|
10
11
|
const pythonTemplateCache = new Map();
|
|
11
12
|
Handlebars.registerHelper('eq', function (a, b) {
|
|
12
13
|
return a === b;
|
|
13
14
|
});
|
|
15
|
+
// Logical OR for block conditionals (e.g. emit a section when either of two
|
|
16
|
+
// param lists is non-empty). Handlebars has no built-in `or`.
|
|
17
|
+
Handlebars.registerHelper('or', function (a, b) {
|
|
18
|
+
return a || b;
|
|
19
|
+
});
|
|
14
20
|
Handlebars.registerHelper('pyDocstring', function (str) {
|
|
15
21
|
if (!str)
|
|
16
22
|
return '';
|
|
17
23
|
return str.replace(/"""/g, '\\"\\"\\"').replace(/\\/g, '\\\\');
|
|
18
24
|
});
|
|
25
|
+
// Escape a value for embedding in a double-quoted Python string literal.
|
|
26
|
+
Handlebars.registerHelper('pyStr', function (str) {
|
|
27
|
+
return escapePyString(String(str ?? ''));
|
|
28
|
+
});
|
|
19
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
30
|
export function renderPythonTemplate(name, data) {
|
|
21
31
|
if (!pythonTemplateCache.has(name)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-template-loader.js","sourceRoot":"","sources":["../../src/emitter/python-template-loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"python-template-loader.js","sourceRoot":"","sources":["../../src/emitter/python-template-loader.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,mBAAmB,GAAG,OAAO,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;AAEnE,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAuC,CAAC;AAE3E,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,4EAA4E;AAC5E,8DAA8D;AAC9D,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,aAAa,EAAE,UAAU,GAAW;IAC5D,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AAEH,yEAAyE;AACzE,UAAU,CAAC,cAAc,CAAC,OAAO,EAAE,UAAU,GAAY;IACvD,OAAO,cAAc,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,IAAS;IAC1D,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,YAAY,GAAG,OAAO,CAAC,mBAAmB,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QACjE,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,mBAAmB,GAAG,GAAG,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,mBAAmB,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import json
|
|
5
5
|
import httpx
|
|
6
|
+
from urllib.parse import quote
|
|
6
7
|
from dotenv import load_dotenv
|
|
7
8
|
from mcp.server import Server
|
|
8
9
|
from mcp.server.stdio import stdio_server
|
|
@@ -37,7 +38,12 @@ API_KEY = os.environ.get("{{envVarName}}", "")
|
|
|
37
38
|
{{/each}}
|
|
38
39
|
|
|
39
40
|
server = Server("{{serverName}}")
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
# Hard per-request deadline (seconds) and response-size cap (bytes). A stalled or
|
|
43
|
+
# runaway upstream otherwise keeps tool calls alive indefinitely or exhausts memory.
|
|
44
|
+
_REQUEST_TIMEOUT_S = float(os.environ.get("MCP_REQUEST_TIMEOUT_MS", "30000")) / 1000.0
|
|
45
|
+
_MAX_RESPONSE_BYTES = int(os.environ.get("MCP_MAX_RESPONSE_BYTES", str(10 * 1024 * 1024)))
|
|
46
|
+
client = httpx.AsyncClient(timeout=_REQUEST_TIMEOUT_S)
|
|
41
47
|
|
|
42
48
|
|
|
43
49
|
def _headers() -> dict[str, str]:
|
|
@@ -48,34 +54,98 @@ def _headers() -> dict[str, str]:
|
|
|
48
54
|
headers["Authorization"] = f"Bearer {BEARER_TOKEN}"
|
|
49
55
|
{{/if}}
|
|
50
56
|
{{#if (eq type "apiKey")}}
|
|
57
|
+
{{#if (eq in "header")}}
|
|
58
|
+
if API_KEY:
|
|
59
|
+
headers["{{pyStr headerName}}"] = API_KEY
|
|
60
|
+
{{/if}}
|
|
61
|
+
{{#if (eq in "cookie")}}
|
|
62
|
+
# API key sent as a cookie — appended to the Cookie header.
|
|
51
63
|
if API_KEY:
|
|
52
|
-
headers
|
|
64
|
+
_existing = headers.get("Cookie")
|
|
65
|
+
_pair = "{{pyStr headerName}}=" + quote(str(API_KEY), safe="")
|
|
66
|
+
headers["Cookie"] = (_existing + "; " + _pair) if _existing else _pair
|
|
67
|
+
{{/if}}
|
|
53
68
|
{{/if}}
|
|
54
69
|
{{/each}}
|
|
55
70
|
return headers
|
|
56
71
|
|
|
57
72
|
|
|
73
|
+
def _auth_query_params() -> dict[str, str]:
|
|
74
|
+
# apiKey-in-query auth: the key is sent as a query parameter, not a header.
|
|
75
|
+
params: dict[str, str] = {}
|
|
76
|
+
{{#each authSchemes}}
|
|
77
|
+
{{#if (eq type "apiKey")}}
|
|
78
|
+
{{#if (eq in "query")}}
|
|
79
|
+
if API_KEY:
|
|
80
|
+
params["{{pyStr headerName}}"] = API_KEY
|
|
81
|
+
{{/if}}
|
|
82
|
+
{{/if}}
|
|
83
|
+
{{/each}}
|
|
84
|
+
return params
|
|
85
|
+
|
|
86
|
+
|
|
58
87
|
{{#each tools}}
|
|
59
88
|
@server.tool()
|
|
60
|
-
async def {{functionName}}({{#each
|
|
89
|
+
async def {{functionName}}({{#each pyPathParams}}{{pyName}}: str, {{/each}}{{#each pyQueryParams}}{{pyName}}: str = "", {{/each}}{{#each pyHeaderParams}}{{pyName}}: str = "", {{/each}}{{#each pyCookieParams}}{{pyName}}: str = "", {{/each}}{{#if hasRequestBody}}body: dict | None = None{{/if}}) -> list[TextContent]:
|
|
61
90
|
"""{{pyDocstring description}}"""
|
|
62
|
-
url =
|
|
63
|
-
{{#
|
|
64
|
-
|
|
91
|
+
url = BASE_URL + "{{pyStr pathTemplate}}"
|
|
92
|
+
{{#each pyPathParams}}
|
|
93
|
+
url = url.replace("{{pyStr brace}}", quote(str({{pyName}}), safe=""))
|
|
94
|
+
{{/each}}
|
|
95
|
+
{{#if pyQueryParams.length}}
|
|
96
|
+
params = { {{#each pyQueryParams}}"{{pyStr apiName}}": {{pyName}}, {{/each}} }
|
|
65
97
|
params = {k: v for k, v in params.items() if v}
|
|
98
|
+
{{#if @root.hasQueryApiKey}}
|
|
99
|
+
params.update(_auth_query_params())
|
|
100
|
+
{{/if}}
|
|
101
|
+
{{else}}
|
|
102
|
+
{{#if @root.hasQueryApiKey}}
|
|
103
|
+
params = _auth_query_params()
|
|
104
|
+
{{/if}}
|
|
105
|
+
{{/if}}
|
|
106
|
+
{{#if (or pyHeaderParams.length pyCookieParams.length)}}
|
|
107
|
+
# Per-request header/cookie params (D-H2): sent under their original wire
|
|
108
|
+
# names, merged over the auth headers. Empty values are omitted.
|
|
109
|
+
req_headers = dict(_headers())
|
|
110
|
+
{{#each pyHeaderParams}}
|
|
111
|
+
if {{pyName}}:
|
|
112
|
+
req_headers["{{pyStr wireName}}"] = {{pyName}}
|
|
113
|
+
{{/each}}
|
|
114
|
+
{{#if pyCookieParams.length}}
|
|
115
|
+
_cookie_parts = []
|
|
116
|
+
{{#each pyCookieParams}}
|
|
117
|
+
if {{pyName}}:
|
|
118
|
+
_cookie_parts.append("{{pyStr wireName}}=" + quote(str({{pyName}}), safe=""))
|
|
119
|
+
{{/each}}
|
|
120
|
+
if _cookie_parts:
|
|
121
|
+
_existing = req_headers.get("Cookie")
|
|
122
|
+
_joined = "; ".join(_cookie_parts)
|
|
123
|
+
req_headers["Cookie"] = (_existing + "; " + _joined) if _existing else _joined
|
|
124
|
+
{{/if}}
|
|
66
125
|
{{/if}}
|
|
67
126
|
try:
|
|
68
127
|
resp = await client.request(
|
|
69
128
|
"{{method}}",
|
|
70
129
|
url,
|
|
71
|
-
headers=_headers(),
|
|
72
|
-
{{#if queryParams.length}}
|
|
130
|
+
headers={{#if (or pyHeaderParams.length pyCookieParams.length)}}req_headers{{else}}_headers(){{/if}},
|
|
131
|
+
{{#if (or queryParams.length @root.hasQueryApiKey)}}
|
|
73
132
|
params=params,
|
|
74
133
|
{{/if}}
|
|
75
134
|
{{#if hasRequestBody}}
|
|
135
|
+
{{#if (eq bodyEncoding "form")}}
|
|
136
|
+
data=body,
|
|
137
|
+
{{else}}
|
|
138
|
+
{{#if (eq bodyEncoding "multipart")}}
|
|
139
|
+
files=body,
|
|
140
|
+
{{else}}
|
|
76
141
|
json=body,
|
|
77
142
|
{{/if}}
|
|
143
|
+
{{/if}}
|
|
144
|
+
{{/if}}
|
|
78
145
|
)
|
|
146
|
+
# Cap the buffered response so a runaway upstream cannot exhaust memory.
|
|
147
|
+
if len(resp.content) > _MAX_RESPONSE_BYTES:
|
|
148
|
+
return [TextContent(type="text", text=f"Error: response exceeded {_MAX_RESPONSE_BYTES}-byte cap")]
|
|
79
149
|
data = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else resp.text
|
|
80
150
|
return [TextContent(type="text", text=json.dumps(data, indent=2) if isinstance(data, (dict, list)) else str(data))]
|
|
81
151
|
except Exception as e:
|
|
@@ -18,6 +18,16 @@ const sessions = new Map<string, BrowserSession>();
|
|
|
18
18
|
let idleTimer: ReturnType<typeof setInterval> | undefined;
|
|
19
19
|
let config: BrowserConfig;
|
|
20
20
|
|
|
21
|
+
// In-flight session creations, keyed by session id. Single-flighting per id
|
|
22
|
+
// means concurrent no-ID calls (which all resolve to DEFAULT_SESSION) and
|
|
23
|
+
// repeated calls with the same explicit id share one Chromium context instead
|
|
24
|
+
// of racing to create several.
|
|
25
|
+
const sessionCreations = new Map<string, Promise<{ page: Page; sessionId: string }>>();
|
|
26
|
+
|
|
27
|
+
// Single-flight the browser launch so concurrent cold starts can't spawn
|
|
28
|
+
// multiple Chromium processes.
|
|
29
|
+
let browserLaunch: Promise<Browser> | undefined;
|
|
30
|
+
|
|
21
31
|
const DEFAULT_SESSION = '__default__';
|
|
22
32
|
|
|
23
33
|
/**
|
|
@@ -78,10 +88,37 @@ export async function resolveSelector(
|
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Launch the shared browser, single-flighting concurrent cold starts so two
|
|
93
|
+
* simultaneous requests can never spawn two Chromium processes.
|
|
94
|
+
*/
|
|
95
|
+
async function getBrowser(): Promise<Browser> {
|
|
96
|
+
if (browser && browser.isConnected()) return browser;
|
|
97
|
+
if (browserLaunch) return browserLaunch;
|
|
98
|
+
|
|
99
|
+
// Only disable sandbox when running as root in a container.
|
|
100
|
+
const isRoot = process.getuid?.() === 0;
|
|
101
|
+
browserLaunch = chromium
|
|
102
|
+
.launch({
|
|
103
|
+
headless: config.headless,
|
|
104
|
+
args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
|
|
105
|
+
})
|
|
106
|
+
.then((b) => {
|
|
107
|
+
browser = b;
|
|
108
|
+
return b;
|
|
109
|
+
})
|
|
110
|
+
.finally(() => {
|
|
111
|
+
browserLaunch = undefined;
|
|
112
|
+
});
|
|
113
|
+
return browserLaunch;
|
|
114
|
+
}
|
|
115
|
+
|
|
81
116
|
/**
|
|
82
117
|
* Get or create a browser session.
|
|
83
118
|
* If sessionId is provided, reuses an existing session or creates one with that ID.
|
|
84
|
-
* If not, creates a
|
|
119
|
+
* If not, reuses (or creates) a single shared default session under a fixed ID,
|
|
120
|
+
* so repeat no-ID calls and `take_screenshot` without an ID all hit the same
|
|
121
|
+
* context instead of leaking a new Chromium context per call.
|
|
85
122
|
* Throws if the session pool is at capacity (maxSessions).
|
|
86
123
|
*/
|
|
87
124
|
export async function getOrCreateSession(sessionId?: string): Promise<{ page: Page; sessionId: string }> {
|
|
@@ -95,7 +132,23 @@ export async function getOrCreateSession(sessionId?: string): Promise<{ page: Pa
|
|
|
95
132
|
return { page: existing.page, sessionId: id };
|
|
96
133
|
}
|
|
97
134
|
|
|
98
|
-
//
|
|
135
|
+
// Coalesce concurrent creations for the same id (notably the default
|
|
136
|
+
// session) onto one promise so we don't create duplicate contexts or
|
|
137
|
+
// double-count capacity.
|
|
138
|
+
const inFlight = sessionCreations.get(id);
|
|
139
|
+
if (inFlight) return inFlight;
|
|
140
|
+
|
|
141
|
+
const creation = createSession(id).finally(() => {
|
|
142
|
+
sessionCreations.delete(id);
|
|
143
|
+
});
|
|
144
|
+
sessionCreations.set(id, creation);
|
|
145
|
+
return creation;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function createSession(id: string): Promise<{ page: Page; sessionId: string }> {
|
|
149
|
+
// Reserve the capacity slot BEFORE any await so concurrent creations of
|
|
150
|
+
// distinct ids can't all pass the gate and then blow past maxSessions. We
|
|
151
|
+
// insert a placeholder entry now and fill in the real handles below.
|
|
99
152
|
const maxSessions = config.maxSessions ?? 10;
|
|
100
153
|
if (sessions.size >= maxSessions) {
|
|
101
154
|
throw new Error(
|
|
@@ -103,38 +156,33 @@ export async function getOrCreateSession(sessionId?: string): Promise<{ page: Pa
|
|
|
103
156
|
'Close an existing session before creating a new one.'
|
|
104
157
|
);
|
|
105
158
|
}
|
|
159
|
+
const reserved: BrowserSession = {
|
|
160
|
+
context: undefined as unknown as BrowserContext,
|
|
161
|
+
page: undefined as unknown as Page,
|
|
162
|
+
lastAccessedAt: Date.now(),
|
|
163
|
+
};
|
|
164
|
+
sessions.set(id, reserved);
|
|
106
165
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
headless: config.headless,
|
|
113
|
-
args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
|
|
166
|
+
try {
|
|
167
|
+
const activeBrowser = await getBrowser();
|
|
168
|
+
const context = await activeBrowser.newContext({
|
|
169
|
+
viewport: config.viewport,
|
|
170
|
+
...(config.userAgent ? { userAgent: config.userAgent } : {}),
|
|
114
171
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Create new session
|
|
118
|
-
const context = await browser.newContext({
|
|
119
|
-
viewport: config.viewport,
|
|
120
|
-
...(config.userAgent ? { userAgent: config.userAgent } : {}),
|
|
121
|
-
});
|
|
122
|
-
const page = await context.newPage();
|
|
172
|
+
const page = await context.newPage();
|
|
123
173
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
: id;
|
|
174
|
+
reserved.context = context;
|
|
175
|
+
reserved.page = page;
|
|
176
|
+
reserved.lastAccessedAt = Date.now();
|
|
128
177
|
|
|
129
|
-
|
|
130
|
-
context,
|
|
131
|
-
page,
|
|
132
|
-
lastAccessedAt: Date.now(),
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
resetIdleTimer();
|
|
178
|
+
resetIdleTimer();
|
|
136
179
|
|
|
137
|
-
|
|
180
|
+
return { page, sessionId: id };
|
|
181
|
+
} catch (err) {
|
|
182
|
+
// Release the reserved slot if context creation failed.
|
|
183
|
+
sessions.delete(id);
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
/**
|
|
@@ -145,7 +193,7 @@ export async function closeSession(sessionId?: string): Promise<void> {
|
|
|
145
193
|
const session = sessions.get(id);
|
|
146
194
|
if (!session) return;
|
|
147
195
|
|
|
148
|
-
await session.context
|
|
196
|
+
await session.context?.close().catch(() => {});
|
|
149
197
|
sessions.delete(id);
|
|
150
198
|
|
|
151
199
|
// If no sessions remain, close the browser
|
|
@@ -161,7 +209,7 @@ export async function closeBrowser(): Promise<void> {
|
|
|
161
209
|
clearIdleTimer();
|
|
162
210
|
|
|
163
211
|
for (const [id, session] of sessions) {
|
|
164
|
-
await session.context
|
|
212
|
+
await session.context?.close().catch(() => {});
|
|
165
213
|
sessions.delete(id);
|
|
166
214
|
}
|
|
167
215
|
|
|
@@ -207,8 +255,9 @@ function resetIdleTimer(): void {
|
|
|
207
255
|
clearIdleTimer();
|
|
208
256
|
idleTimer = setInterval(() => {
|
|
209
257
|
const now = Date.now();
|
|
210
|
-
// Close idle sessions
|
|
258
|
+
// Close idle sessions. Skip slots still being created (no context yet).
|
|
211
259
|
for (const [id, session] of sessions) {
|
|
260
|
+
if (!session.context) continue;
|
|
212
261
|
if (now - session.lastAccessedAt > config.idleTimeoutMs) {
|
|
213
262
|
session.context.close().catch(() => {});
|
|
214
263
|
sessions.delete(id);
|
|
@@ -14,12 +14,19 @@ function log(level: string, message: string, data?: Record<string, unknown>): vo
|
|
|
14
14
|
const config = loadConfig();
|
|
15
15
|
initBrowserManager(config.browser);
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
/* Build a fully-registered McpServer. A single McpServer connected to multiple
|
|
18
|
+
* transports concurrently races on its internal transport reference (the API
|
|
19
|
+
* HTTP template documents the same hazard), so HTTP requests get a fresh,
|
|
20
|
+
* stateless server + transport per request instead of sharing one global
|
|
21
|
+
* instance. */
|
|
22
|
+
function createMcpServer(): McpServer {
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: '{{serverName}}',
|
|
25
|
+
version: '{{serverVersion}}',
|
|
26
|
+
});
|
|
27
|
+
registerAllTools(server, config);
|
|
28
|
+
return server;
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
let isReady = false;
|
|
25
32
|
|
|
@@ -72,8 +79,17 @@ if (process.env.TRANSPORT === 'http') {
|
|
|
72
79
|
}
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
// Fresh stateless server + transport per request. Both are closed when
|
|
83
|
+
// the response finishes or the connection aborts, so transports and
|
|
84
|
+
// their listeners are never leaked and concurrent requests can't
|
|
85
|
+
// misroute through a shared transport reference.
|
|
86
|
+
const reqServer = createMcpServer();
|
|
75
87
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
76
|
-
|
|
88
|
+
res.on('close', () => {
|
|
89
|
+
void transport.close();
|
|
90
|
+
void reqServer.close();
|
|
91
|
+
});
|
|
92
|
+
await reqServer.connect(transport);
|
|
77
93
|
await transport.handleRequest(req, res);
|
|
78
94
|
return;
|
|
79
95
|
}
|
|
@@ -97,6 +113,8 @@ if (process.env.TRANSPORT === 'http') {
|
|
|
97
113
|
process.on('SIGTERM', shutdown);
|
|
98
114
|
process.on('SIGINT', shutdown);
|
|
99
115
|
} else {
|
|
116
|
+
// Stdio is inherently single-connection, so one long-lived server is correct.
|
|
117
|
+
const server = createMcpServer();
|
|
100
118
|
const transport = new StdioServerTransport();
|
|
101
119
|
await server.connect(transport);
|
|
102
120
|
isReady = true;
|
|
@@ -45,8 +45,8 @@ export function register(server: McpServer, config: AppConfig): void {
|
|
|
45
45
|
{{#each form.fields}}
|
|
46
46
|
{{#unless (eq fieldType "hidden")}}
|
|
47
47
|
{{#if (eq fieldType "checkbox")}}
|
|
48
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
49
|
-
const checked = Boolean((input as Record<string, unknown>)['{{jsString
|
|
48
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
49
|
+
const checked = Boolean((input as Record<string, unknown>)['{{jsString inputKey}}']);
|
|
50
50
|
const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
|
|
51
51
|
if (checked) {
|
|
52
52
|
await page.check(fieldSelector);
|
|
@@ -55,21 +55,23 @@ export function register(server: McpServer, config: AppConfig): void {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
{{else if (eq fieldType "select")}}
|
|
58
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
58
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
59
59
|
const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
|
|
60
|
-
|
|
60
|
+
// The schema enum is built from option VALUES, and selectOption
|
|
61
|
+
// matches by value, so pass the value through directly.
|
|
62
|
+
await page.selectOption(fieldSelector, String((input as Record<string, unknown>)['{{jsString inputKey}}']));
|
|
61
63
|
}
|
|
62
64
|
{{else if (eq fieldType "radio")}}
|
|
63
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
65
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
64
66
|
const radioName = '{{jsString name}}';
|
|
65
|
-
const radioValue = String((input as Record<string, unknown>)['{{jsString
|
|
67
|
+
const radioValue = String((input as Record<string, unknown>)['{{jsString inputKey}}']).replace(/["\\[\]()]/g, '\\$&');
|
|
66
68
|
const radioNameSel = radioName.replace(/["\\]/g, '\\$&');
|
|
67
69
|
await page.check(`input[name="${radioNameSel}"][value="${radioValue}"]`);
|
|
68
70
|
}
|
|
69
71
|
{{else}}
|
|
70
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
72
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
71
73
|
const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
|
|
72
|
-
await page.fill(fieldSelector, String((input as Record<string, unknown>)['{{jsString
|
|
74
|
+
await page.fill(fieldSelector, String((input as Record<string, unknown>)['{{jsString inputKey}}']));
|
|
73
75
|
}
|
|
74
76
|
{{/if}}
|
|
75
77
|
{{/unless}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AA8BA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAU9D"}
|
|
@@ -13,6 +13,15 @@ Handlebars.registerHelper('capitalize', function (str) {
|
|
|
13
13
|
return str;
|
|
14
14
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
15
15
|
});
|
|
16
|
+
// Emit a value as a valid JS/TS literal via JSON.stringify. Safe for embedding
|
|
17
|
+
// untrusted spec-derived strings (e.g. OpenAPI security-scheme names) into
|
|
18
|
+
// generated code: JSON.stringify produces a double-quoted literal with all
|
|
19
|
+
// special characters escaped, so the value can never break out of the literal.
|
|
20
|
+
// Registered on the shared Handlebars singleton, so worker templates (which
|
|
21
|
+
// import the same instance) can use it too.
|
|
22
|
+
Handlebars.registerHelper('json', function (value) {
|
|
23
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
24
|
+
});
|
|
16
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
26
|
export function renderTemplate(name, data) {
|
|
18
27
|
if (!templateCache.has(name)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-loader.js","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAErD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAC;AAErE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,GAAW;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAS;IACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
|
|
1
|
+
{"version":3,"file":"template-loader.js","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAErD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAC;AAErE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,GAAW;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,+EAA+E;AAC/E,4EAA4E;AAC5E,4CAA4C;AAC5C,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,KAAc;IACxD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAS;IACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -3,51 +3,88 @@ import type { AppConfig } from './config.js';
|
|
|
3
3
|
import { getAccessToken } from './oauth.js';
|
|
4
4
|
{{/if}}
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Per-operation outbound-auth requirement (D-H2), forwarded from the tool
|
|
8
|
+
* handler. Mirrors the generator's ToolAuthRequirement:
|
|
9
|
+
* - undefined → apply every configured scheme (global default).
|
|
10
|
+
* - { mode: 'public' } → apply NO auth (operation declared `security: []`).
|
|
11
|
+
* - { mode: 'schemes' } → apply ONLY the listed OpenAPI scheme names.
|
|
12
|
+
*/
|
|
13
|
+
export type AuthRequirement =
|
|
14
|
+
| { mode: 'public' }
|
|
15
|
+
| { mode: 'schemes'; schemeNames: string[] };
|
|
16
|
+
|
|
17
|
+
/** Whether the scheme named `name` should be applied for this requirement. */
|
|
18
|
+
function schemeApplies(req: AuthRequirement | undefined, name: string): boolean {
|
|
19
|
+
if (!req) return true; // no per-op requirement → legacy global behavior
|
|
20
|
+
if (req.mode === 'public') return false; // explicit `security: []`
|
|
21
|
+
return req.schemeNames.includes(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
{{#if hasOAuth}}
|
|
7
|
-
export async function getAuthHeaders(
|
|
25
|
+
export async function getAuthHeaders(
|
|
26
|
+
config: AppConfig,
|
|
27
|
+
requirement?: AuthRequirement,
|
|
28
|
+
): Promise<Record<string, string>> {
|
|
8
29
|
{{else}}
|
|
9
|
-
export function getAuthHeaders(
|
|
30
|
+
export function getAuthHeaders(
|
|
31
|
+
config: AppConfig,
|
|
32
|
+
requirement?: AuthRequirement,
|
|
33
|
+
): Record<string, string> {
|
|
10
34
|
{{/if}}
|
|
11
35
|
const headers: Record<string, string> = {};
|
|
12
36
|
|
|
13
37
|
{{#each authSchemes}}
|
|
14
38
|
{{#if (eq type "apiKey")}}
|
|
15
39
|
{{#if (eq in "header")}}
|
|
16
|
-
if (config.apiKey) {
|
|
40
|
+
if (config.apiKey && schemeApplies(requirement, {{{json schemeName}}})) {
|
|
17
41
|
headers['{{headerName}}'] = config.apiKey;
|
|
18
42
|
}
|
|
19
43
|
{{/if}}
|
|
44
|
+
{{#if (eq in "cookie")}}
|
|
45
|
+
// API key sent as a cookie — appended to the Cookie header so it survives
|
|
46
|
+
// alongside any per-operation cookie parameters set by the handler.
|
|
47
|
+
if (config.apiKey && schemeApplies(requirement, {{{json schemeName}}})) {
|
|
48
|
+
const existing = headers['Cookie'];
|
|
49
|
+
const pair = '{{headerName}}=' + encodeURIComponent(config.apiKey);
|
|
50
|
+
headers['Cookie'] = existing ? existing + '; ' + pair : pair;
|
|
51
|
+
}
|
|
52
|
+
{{/if}}
|
|
20
53
|
{{#if (eq in "query")}}
|
|
21
|
-
// API key sent as query parameter
|
|
54
|
+
// API key is sent as a query parameter (`{{headerName}}`). Query-string auth is
|
|
55
|
+
// applied during URL construction (apiKeyQueryName/apiKeyQueryValue in config),
|
|
56
|
+
// not as a header, so there is nothing to add here.
|
|
22
57
|
{{/if}}
|
|
23
58
|
{{/if}}
|
|
24
59
|
{{#if (eq type "http-bearer")}}
|
|
25
|
-
if (config.bearerToken) {
|
|
60
|
+
if (config.bearerToken && schemeApplies(requirement, {{{json schemeName}}})) {
|
|
26
61
|
headers['Authorization'] = `Bearer ${config.bearerToken}`;
|
|
27
62
|
}
|
|
28
63
|
{{/if}}
|
|
29
64
|
{{#if (eq type "http-basic")}}
|
|
30
|
-
if (config.basicUsername && config.basicPassword) {
|
|
65
|
+
if (config.basicUsername && config.basicPassword && schemeApplies(requirement, {{{json schemeName}}})) {
|
|
31
66
|
const encoded = Buffer.from(`${config.basicUsername}:${config.basicPassword}`).toString('base64');
|
|
32
67
|
headers['Authorization'] = `Basic ${encoded}`;
|
|
33
68
|
}
|
|
34
69
|
{{/if}}
|
|
35
70
|
{{#if (eq type "oauth2")}}
|
|
36
71
|
// OAuth2 — use token management with refresh
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
72
|
+
if (schemeApplies(requirement, {{{json schemeName}}})) {
|
|
73
|
+
try {
|
|
74
|
+
const token = await getAccessToken({
|
|
75
|
+
clientId: config.oauth2ClientId ?? '',
|
|
76
|
+
clientSecret: config.oauth2ClientSecret,
|
|
77
|
+
authorizationUrl: config.oauth2AuthorizationUrl ?? '',
|
|
78
|
+
tokenUrl: config.oauth2TokenUrl ?? '',
|
|
79
|
+
scopes: [],
|
|
80
|
+
redirectUri: config.oauth2RedirectUri ?? 'http://localhost:3000/callback',
|
|
81
|
+
});
|
|
82
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
83
|
+
} catch {
|
|
84
|
+
// Fallback to pre-obtained token
|
|
85
|
+
if (config.oauth2Token) {
|
|
86
|
+
headers['Authorization'] = `Bearer ${config.oauth2Token}`;
|
|
87
|
+
}
|
|
51
88
|
}
|
|
52
89
|
}
|
|
53
90
|
{{/if}}
|
|
@@ -55,3 +92,27 @@ export function getAuthHeaders(config: AppConfig): Record<string, string> {
|
|
|
55
92
|
|
|
56
93
|
return headers;
|
|
57
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Query-string auth parameters (apiKey-in-query schemes), keyed by query-param
|
|
98
|
+
* name. Applied by the tool handler during URL construction — a query parameter
|
|
99
|
+
* cannot be sent as a request header (D-H2). Respects the same per-operation
|
|
100
|
+
* requirement as the headers: a public operation gets none, a scheme-scoped one
|
|
101
|
+
* gets only its scheme(s).
|
|
102
|
+
*/
|
|
103
|
+
export function getAuthQueryParams(
|
|
104
|
+
config: AppConfig,
|
|
105
|
+
requirement?: AuthRequirement,
|
|
106
|
+
): Record<string, string> {
|
|
107
|
+
const params: Record<string, string> = {};
|
|
108
|
+
{{#each authSchemes}}
|
|
109
|
+
{{#if (eq type "apiKey")}}
|
|
110
|
+
{{#if (eq in "query")}}
|
|
111
|
+
if (config.apiKey && config.apiKeyQueryName && schemeApplies(requirement, {{{json schemeName}}})) {
|
|
112
|
+
params[config.apiKeyQueryName] = config.apiKey;
|
|
113
|
+
}
|
|
114
|
+
{{/if}}
|
|
115
|
+
{{/if}}
|
|
116
|
+
{{/each}}
|
|
117
|
+
return params;
|
|
118
|
+
}
|
|
@@ -3,6 +3,10 @@ export interface AppConfig {
|
|
|
3
3
|
{{#each authSchemes}}
|
|
4
4
|
{{#if (eq type "apiKey")}}
|
|
5
5
|
apiKey?: string;
|
|
6
|
+
{{#if (eq in "query")}}
|
|
7
|
+
/** Query-parameter name the API key is appended under (apiKey-in-query auth). */
|
|
8
|
+
apiKeyQueryName?: string;
|
|
9
|
+
{{/if}}
|
|
6
10
|
{{/if}}
|
|
7
11
|
{{#if (eq type "http-bearer")}}
|
|
8
12
|
bearerToken?: string;
|
|
@@ -78,6 +82,9 @@ export function loadConfig(): AppConfig {
|
|
|
78
82
|
{{#each authSchemes}}
|
|
79
83
|
{{#if (eq type "apiKey")}}
|
|
80
84
|
apiKey: process.env.{{envVarName}},
|
|
85
|
+
{{#if (eq in "query")}}
|
|
86
|
+
apiKeyQueryName: {{{json headerName}}},
|
|
87
|
+
{{/if}}
|
|
81
88
|
{{/if}}
|
|
82
89
|
{{#if (eq type "http-bearer")}}
|
|
83
90
|
bearerToken: process.env.{{envVarName}},
|
|
@@ -49,4 +49,10 @@ MCP_AUTH_TOKEN=
|
|
|
49
49
|
# — any request can hit any instance). Set to "true" for stateful per-session
|
|
50
50
|
# (Mcp-Session-Id) mode if a client needs resumable streams.
|
|
51
51
|
MCP_STATEFUL=
|
|
52
|
+
# Stateful-session bounds (only used when MCP_STATEFUL=true). The server caps the
|
|
53
|
+
# number of live sessions, evicts ones idle past MCP_SESSION_IDLE_MS, and sweeps
|
|
54
|
+
# on a MCP_SESSION_REAP_MS timer so abandoned sessions can't exhaust memory.
|
|
55
|
+
# MCP_MAX_SESSIONS=1000
|
|
56
|
+
# MCP_SESSION_IDLE_MS=300000
|
|
57
|
+
# MCP_SESSION_REAP_MS=60000
|
|
52
58
|
{{/if}}
|