@ateam-ai/mcp 0.4.25 → 0.4.27
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/package.json +3 -2
- package/src/api.js +29 -0
- package/src/http.js +29 -3
- package/src/tools.js +111 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateam-ai/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"mcpName": "io.github.ariekogan/ateam-mcp",
|
|
5
5
|
"description": "A-Team MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"start": "node src/index.js",
|
|
13
13
|
"start:http": "node src/index.js --http",
|
|
14
14
|
"dev": "node --watch src/index.js",
|
|
15
|
-
"dev:http": "node --watch src/index.js --http"
|
|
15
|
+
"dev:http": "node --watch src/index.js --http",
|
|
16
|
+
"test": "node test/session-isolation.test.mjs"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"mcp",
|
package/src/api.js
CHANGED
|
@@ -211,6 +211,35 @@ export function bindSessionBearer(sessionId, bearerToken) {
|
|
|
211
211
|
console.log(`[Auth] Bearer bound for session ${sessionId}`);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The bearer a session is bound to, or null if the session was never
|
|
216
|
+
* bearer-authenticated (e.g. a no-bearer ateam_auth flow). Used by the HTTP
|
|
217
|
+
* transport to enforce that a bearer-bound session can only be reused by a
|
|
218
|
+
* request presenting the SAME validated bearer — a client-supplied session-id
|
|
219
|
+
* alone must never grant access to another client's credentials.
|
|
220
|
+
*/
|
|
221
|
+
export function getSessionBearer(sessionId) {
|
|
222
|
+
return sessionBearers.get(sessionId) || null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* May a request presenting `presentedToken` (its validated bearer, or
|
|
227
|
+
* null/undefined if none) reuse a session whose bound bearer is `boundBearer`?
|
|
228
|
+
*
|
|
229
|
+
* - No bound bearer → the session was never bearer-authenticated (a no-bearer
|
|
230
|
+
* ateam_auth flow); nothing to match against, allow (unchanged behavior).
|
|
231
|
+
* - Bound bearer → the request MUST present the exact same validated bearer.
|
|
232
|
+
* A missing or different bearer is denied — so a client that knows another
|
|
233
|
+
* client's (non-secret, logged/echoed) session-id cannot be served that
|
|
234
|
+
* client's credentials by sending the id with no/other Authorization.
|
|
235
|
+
*
|
|
236
|
+
* Pure + exported for unit testing.
|
|
237
|
+
*/
|
|
238
|
+
export function bearerOwnershipOk(boundBearer, presentedToken) {
|
|
239
|
+
if (!boundBearer) return true;
|
|
240
|
+
return !!presentedToken && presentedToken === boundBearer;
|
|
241
|
+
}
|
|
242
|
+
|
|
214
243
|
/** Store ateam_auth override for this user (by bearer). Called from tools.js. */
|
|
215
244
|
export function setAuthOverride(sessionId, { tenant, apiKey, apiUrl }) {
|
|
216
245
|
const bearer = sessionBearers.get(sessionId);
|
package/src/http.js
CHANGED
|
@@ -26,7 +26,7 @@ import { createServer } from "./server.js";
|
|
|
26
26
|
import {
|
|
27
27
|
clearSession, setSessionCredentials, parseApiKey,
|
|
28
28
|
startSessionSweeper, getSessionStats, sweepStaleSessions,
|
|
29
|
-
bindSessionBearer, getAuthOverride,
|
|
29
|
+
bindSessionBearer, getAuthOverride, getSessionBearer, bearerOwnershipOk,
|
|
30
30
|
} from "./api.js";
|
|
31
31
|
import { mountOAuth } from "./oauth.js";
|
|
32
32
|
import { connectGithubPage } from "./pages.js";
|
|
@@ -222,9 +222,10 @@ export function startHttpServer(port = 3100) {
|
|
|
222
222
|
});
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
// ─── Get API Key — redirect to
|
|
225
|
+
// ─── Get API Key — redirect to the main web app's Tenant Admin →
|
|
226
|
+
// Tokens & Keys (the key now lives in the main UI, not the builder). ──
|
|
226
227
|
app.get("/get-api-key", (_req, res) => {
|
|
227
|
-
res.redirect("https://app.ateam-ai.com
|
|
228
|
+
res.redirect("https://app.ateam-ai.com/?admin=tokens");
|
|
228
229
|
});
|
|
229
230
|
|
|
230
231
|
// ─── Connect GitHub — user-facing guide an agent links to on
|
|
@@ -235,8 +236,31 @@ export function startHttpServer(port = 3100) {
|
|
|
235
236
|
|
|
236
237
|
// ─── MCP POST — handle tool calls + initialize ───────────────
|
|
237
238
|
// Mounted at both "/" and "/mcp" for Claude.ai compatibility
|
|
239
|
+
// SECURITY (multi-client isolation): a session-id is client-supplied and
|
|
240
|
+
// non-secret (logged + echoed in the mcp-session-id response header). If a
|
|
241
|
+
// session was authenticated with a Bearer, ONLY a request presenting that SAME
|
|
242
|
+
// validated Bearer may reuse it — for POST (tool calls), GET (SSE stream) and
|
|
243
|
+
// DELETE (terminate). Without this, a client could send another client's
|
|
244
|
+
// session-id on the optional-auth /mcp path with no/other Authorization and be
|
|
245
|
+
// served that client's tenant + api key (or read its stream / kill its
|
|
246
|
+
// session). A session with no bound bearer (the no-bearer ateam_auth flow) has
|
|
247
|
+
// nothing to match against, so this is a no-op there.
|
|
248
|
+
const denySessionReuse = (req, res, sessionId) => {
|
|
249
|
+
if (sessionId && !bearerOwnershipOk(getSessionBearer(sessionId), req.auth?.token)) {
|
|
250
|
+
console.warn(`[Auth] DENY session reuse: bearer mismatch for session ${sessionId} (presented=${req.auth?.token ? "other-bearer" : "none"})`);
|
|
251
|
+
res.status(401).json({
|
|
252
|
+
jsonrpc: "2.0",
|
|
253
|
+
error: { code: -32001, message: "Unauthorized: this session belongs to a different credential. Re-initialize with your own Authorization." },
|
|
254
|
+
id: req.body?.id ?? null,
|
|
255
|
+
});
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
};
|
|
260
|
+
|
|
238
261
|
const mcpPost = async (req, res) => {
|
|
239
262
|
const sessionId = req.headers["mcp-session-id"];
|
|
263
|
+
if (denySessionReuse(req, res, sessionId)) return;
|
|
240
264
|
|
|
241
265
|
try {
|
|
242
266
|
let transport;
|
|
@@ -345,6 +369,7 @@ export function startHttpServer(port = 3100) {
|
|
|
345
369
|
res.json({ ok: true, service: "ateam-mcp", transport: "http" });
|
|
346
370
|
return;
|
|
347
371
|
}
|
|
372
|
+
if (denySessionReuse(req, res, sessionId)) return;
|
|
348
373
|
await transports[sessionId].handleRequest(req, res);
|
|
349
374
|
};
|
|
350
375
|
|
|
@@ -355,6 +380,7 @@ export function startHttpServer(port = 3100) {
|
|
|
355
380
|
res.status(400).send("Invalid or missing session ID");
|
|
356
381
|
return;
|
|
357
382
|
}
|
|
383
|
+
if (denySessionReuse(req, res, sessionId)) return;
|
|
358
384
|
await transports[sessionId].handleRequest(req, res);
|
|
359
385
|
};
|
|
360
386
|
|
package/src/tools.js
CHANGED
|
@@ -332,9 +332,9 @@ export const tools = [
|
|
|
332
332
|
properties: {
|
|
333
333
|
topic: {
|
|
334
334
|
type: "string",
|
|
335
|
-
enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers", "widgets"],
|
|
335
|
+
enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers", "widgets", "ui-plugins"],
|
|
336
336
|
description:
|
|
337
|
-
"What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide, 'python_helpers' = adas.* helper namespace for run_python_script orchestration (read this when designing personas that read state → call tools → checkpoint → status; without it, scripts hand-roll JSON parsing and tool delegation = 5-10x larger and brittler), 'widgets' = widget (UI plugin) spec: catalog model, how_to_use block shape (solution.json snippet + opener_call + persona_phrasing + binding_notes), and rules for declaring ui_plugins. Pair with ateam_get_widget_catalog for the live per-tenant inventory.",
|
|
337
|
+
"What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide, 'python_helpers' = adas.* helper namespace for run_python_script orchestration (read this when designing personas that read state → call tools → checkpoint → status; without it, scripts hand-roll JSON parsing and tool delegation = 5-10x larger and brittler), 'widgets' = widget (UI plugin) spec: catalog model, how_to_use block shape (solution.json snippet + opener_call + persona_phrasing + binding_notes), and rules for declaring ui_plugins. Pair with ateam_get_widget_catalog for the live per-tenant inventory. 'ui-plugins' = the DEEP React Native (mobile) plugin build guide: author in rn-src/, compile with a build:rn esbuild script (format=cjs, target=es2015, external react/react-native/@adas/plugin-sdk) to rn-bundle/index.bundle.js, plain-object export — read this before authoring any MOBILE widget.",
|
|
338
338
|
},
|
|
339
339
|
section: {
|
|
340
340
|
type: "string",
|
|
@@ -371,9 +371,9 @@ export const tools = [
|
|
|
371
371
|
properties: {
|
|
372
372
|
type: {
|
|
373
373
|
type: "string",
|
|
374
|
-
enum: ["skill", "connector", "connector-ui", "solution", "script-cache-skill", "index"],
|
|
374
|
+
enum: ["skill", "connector", "connector-ui", "solution", "script-cache-skill", "ui-plugin-native", "index"],
|
|
375
375
|
description:
|
|
376
|
-
"Example type: 'skill' = Order Support Agent, 'connector' = stdio MCP connector, 'connector-ui' = UI-capable connector, 'solution' = full 3-skill e-commerce solution, 'script-cache-skill' = fat-tool skill with script_cache opt-in (reference implementation of script-level JIT shortcuts — study this before building any browser-automation skill), 'index' = list all available examples",
|
|
376
|
+
"Example type: 'skill' = Order Support Agent, 'connector' = stdio MCP connector, 'connector-ui' = UI-capable connector, 'solution' = full 3-skill e-commerce solution, 'script-cache-skill' = fat-tool skill with script_cache opt-in (reference implementation of script-level JIT shortcuts — study this before building any browser-automation skill), 'ui-plugin-native' = complete working React Native (mobile) UI plugin (rn-src/index.tsx + esbuild build:rn → rn-bundle, @adas/plugin-sdk, es2015), 'index' = list all available examples",
|
|
377
377
|
},
|
|
378
378
|
},
|
|
379
379
|
required: ["type"],
|
|
@@ -1862,6 +1862,7 @@ const SPEC_PATHS = {
|
|
|
1862
1862
|
"connector-multi-user": "/spec/multi-user-connector",
|
|
1863
1863
|
python_helpers: "/spec/python_helpers",
|
|
1864
1864
|
widgets: "/spec/widgets",
|
|
1865
|
+
"ui-plugins": "/spec/ui-plugins",
|
|
1865
1866
|
};
|
|
1866
1867
|
|
|
1867
1868
|
const EXAMPLE_PATHS = {
|
|
@@ -1871,6 +1872,7 @@ const EXAMPLE_PATHS = {
|
|
|
1871
1872
|
"connector-ui": "/spec/examples/connector-ui",
|
|
1872
1873
|
solution: "/spec/examples/solution",
|
|
1873
1874
|
"script-cache-skill": "/spec/examples/script-cache-skill",
|
|
1875
|
+
"ui-plugin-native": "/spec/examples/ui-plugin-native",
|
|
1874
1876
|
};
|
|
1875
1877
|
|
|
1876
1878
|
// Tools that are tenant-aware — require EXPLICIT ateam_auth (env vars alone not enough).
|
|
@@ -2102,10 +2104,15 @@ the \`tools/call\` dispatch. Data tools are per-actor — call \`getActorId(args
|
|
|
2102
2104
|
## Adding UI plugins (ui_capable connectors)
|
|
2103
2105
|
|
|
2104
2106
|
Use \`ateam_create_plugin\` (or drop the files yourself): iframe plugins go under
|
|
2105
|
-
\`ui-dist/<plugin-name>/index.html\` with a \`ui-dist/<plugin-name>/manifest.json
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
plugin
|
|
2107
|
+
\`ui-dist/<plugin-name>/index.html\` with a \`ui-dist/<plugin-name>/manifest.json\`.
|
|
2108
|
+
RN plugins have editable source at \`rn-src/<plugin-name>.tsx\` (imported from
|
|
2109
|
+
\`@adas/plugin-sdk\`) AND a PRE-BUILT, COMMITTED bundle at
|
|
2110
|
+
\`rn-bundle/<plugin-name>.bundle.js\` — the mobile
|
|
2111
|
+
app downloads the bundle, and Core never compiles the .tsx (deploys run
|
|
2112
|
+
\`npm install --no-optional\`, which skips esbuild). After editing the .tsx,
|
|
2113
|
+
rebuild the bundle with the esbuild command in its header and commit it. This
|
|
2114
|
+
connector's \`ui.listPlugins\` / \`ui.getPlugin\` read the manifests at call time,
|
|
2115
|
+
so a new plugin renders with NO server.js edit.
|
|
2109
2116
|
|
|
2110
2117
|
## Deploy
|
|
2111
2118
|
|
|
@@ -2187,15 +2194,24 @@ function _scaffoldPluginFiles({ connectorId, pluginName, kind }) {
|
|
|
2187
2194
|
}
|
|
2188
2195
|
|
|
2189
2196
|
if (kind === "rn" || kind === "adaptive") {
|
|
2190
|
-
const tsx = `// ${pluginName} — React Native plugin. Generated by ateam_create_plugin.
|
|
2197
|
+
const tsx = `// ${pluginName} — React Native plugin SOURCE (editable). Generated by ateam_create_plugin.
|
|
2191
2198
|
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2199
|
+
// ⚠️ Core does NOT compile this .tsx. Deploys run "npm install --production
|
|
2200
|
+
// --no-optional" (which skips esbuild's platform binary) and only ever run a
|
|
2201
|
+
// "build" script — never "build:rn". The mobile app therefore loads the
|
|
2202
|
+
// PRE-BUILT, COMMITTED bundle at rn-bundle/${pluginName}.bundle.js, NOT this
|
|
2203
|
+
// file. cp.getContextPlugin advertises reactNative.bundleUrl only when that
|
|
2204
|
+
// bundle exists on disk — no bundle → mobile has nothing to download.
|
|
2205
|
+
//
|
|
2206
|
+
// After editing this file, rebuild the bundle and commit it (target=es2015 is
|
|
2207
|
+
// REQUIRED — the mobile runtime evals the bundle with new Function(), which
|
|
2208
|
+
// cannot parse async/await; es2015 downlevels it):
|
|
2209
|
+
// npx esbuild rn-src/${pluginName}.tsx --bundle --format=cjs --platform=neutral --target=es2015 --external:react --external:react-native --external:@adas/plugin-sdk --outfile=rn-bundle/${pluginName}.bundle.js
|
|
2194
2210
|
|
|
2195
2211
|
import React, { useState } from 'react';
|
|
2196
2212
|
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
2197
|
-
import { useApi } from '
|
|
2198
|
-
import type { PluginProps } from '
|
|
2213
|
+
import { useApi } from '@adas/plugin-sdk';
|
|
2214
|
+
import type { PluginProps } from '@adas/plugin-sdk';
|
|
2199
2215
|
|
|
2200
2216
|
// Plain object export — NO PluginSDK.register() (pollutes shared registry).
|
|
2201
2217
|
export default {
|
|
@@ -2214,7 +2230,7 @@ export default {
|
|
|
2214
2230
|
const result = await api.call('${connectorId}.echo', { message: 'hello' });
|
|
2215
2231
|
setOutput(JSON.stringify(result, null, 2));
|
|
2216
2232
|
} catch (err: any) {
|
|
2217
|
-
native?.haptics?.
|
|
2233
|
+
native?.haptics?.notification?.('error');
|
|
2218
2234
|
setOutput('Error: ' + err.message);
|
|
2219
2235
|
}
|
|
2220
2236
|
};
|
|
@@ -2246,9 +2262,88 @@ export default {
|
|
|
2246
2262
|
};
|
|
2247
2263
|
`;
|
|
2248
2264
|
files.push({
|
|
2249
|
-
path: `
|
|
2265
|
+
path: `rn-src/${pluginName}.tsx`,
|
|
2250
2266
|
content: tsx,
|
|
2251
2267
|
});
|
|
2268
|
+
|
|
2269
|
+
// Pre-built RN bundle — THIS is the file the mobile app actually downloads.
|
|
2270
|
+
// cp.getContextPlugin only advertises reactNative.bundleUrl when a
|
|
2271
|
+
// rn-bundle/<pluginId>.bundle.js (or index.bundle.js) exists on disk; the
|
|
2272
|
+
// deploy pipeline can't produce it (npm install --no-optional skips esbuild,
|
|
2273
|
+
// and only a "build" script would run — not "build:rn"), so we SHIP it
|
|
2274
|
+
// pre-built and committed. Without this file the manifest carries
|
|
2275
|
+
// render.reactNative.component but no bundleUrl and mobile renders nothing
|
|
2276
|
+
// (the web iframe still works). Named per-plugin (matches cp.getContextPlugin's
|
|
2277
|
+
// \`${'$'}{pluginId}.bundle.js\` primary lookup) so multiple RN widgets can
|
|
2278
|
+
// coexist in one connector. Kept in sync with the .tsx above via the esbuild
|
|
2279
|
+
// command in its header. es2015 CJS, plain-object default export, no
|
|
2280
|
+
// async/await — passes the mobile new Function() load test.
|
|
2281
|
+
const rnBundle = `"use strict";
|
|
2282
|
+
// ${pluginName} — pre-built React Native bundle (es2015 CJS). Generated by
|
|
2283
|
+
// ateam_create_plugin from rn-src/${pluginName}.tsx. DO NOT hand-edit —
|
|
2284
|
+
// edit the .tsx and rerun the esbuild command in its header, then commit this.
|
|
2285
|
+
var React = require("react");
|
|
2286
|
+
var ReactNative = require("react-native");
|
|
2287
|
+
var sdk = require("@adas/plugin-sdk");
|
|
2288
|
+
var useState = React.useState;
|
|
2289
|
+
var h = React.createElement;
|
|
2290
|
+
var View = ReactNative.View;
|
|
2291
|
+
var Text = ReactNative.Text;
|
|
2292
|
+
var TouchableOpacity = ReactNative.TouchableOpacity;
|
|
2293
|
+
var StyleSheet = ReactNative.StyleSheet;
|
|
2294
|
+
var useApi = sdk.useApi;
|
|
2295
|
+
|
|
2296
|
+
function Component(props) {
|
|
2297
|
+
var native = props.native, theme = props.theme;
|
|
2298
|
+
var api = useApi(props.bridge);
|
|
2299
|
+
var _s = useState(""), output = _s[0], setOutput = _s[1];
|
|
2300
|
+
|
|
2301
|
+
function handlePress() {
|
|
2302
|
+
if (native && native.haptics && native.haptics.selection) native.haptics.selection();
|
|
2303
|
+
api.call("${connectorId}.echo", { message: "hello" }).then(function (result) {
|
|
2304
|
+
setOutput(JSON.stringify(result, null, 2));
|
|
2305
|
+
}, function (err) {
|
|
2306
|
+
if (native && native.haptics && native.haptics.notification) native.haptics.notification("error");
|
|
2307
|
+
setOutput("Error: " + (err && err.message ? err.message : String(err)));
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
var c = theme.colors;
|
|
2312
|
+
var styles = StyleSheet.create({
|
|
2313
|
+
container: { padding: 16, backgroundColor: c.bg, flex: 1 },
|
|
2314
|
+
card: { backgroundColor: c.surface, padding: 12, borderRadius: 8 },
|
|
2315
|
+
title: { fontSize: 18, fontWeight: "600", color: c.text, marginBottom: 8 },
|
|
2316
|
+
button: { backgroundColor: c.accent, padding: 12, borderRadius: 6, marginTop: 12 },
|
|
2317
|
+
buttonText: { color: "#fff", textAlign: "center", fontWeight: "600" },
|
|
2318
|
+
output: { color: c.textMuted, marginTop: 12, fontFamily: "Menlo" }
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
return h(View, { style: styles.container },
|
|
2322
|
+
h(View, { style: styles.card },
|
|
2323
|
+
h(Text, { style: styles.title }, "${pluginName}"),
|
|
2324
|
+
h(Text, { style: { color: c.textMuted } }, "Plugin body — replace with your real UI."),
|
|
2325
|
+
h(TouchableOpacity, { style: styles.button, onPress: handlePress },
|
|
2326
|
+
h(Text, { style: styles.buttonText }, "Call sample tool")),
|
|
2327
|
+
output ? h(Text, { style: styles.output }, output) : null
|
|
2328
|
+
)
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
var plugin = {
|
|
2333
|
+
id: "${pluginName}",
|
|
2334
|
+
type: "ui",
|
|
2335
|
+
version: "1.0.0",
|
|
2336
|
+
capabilities: { haptics: true },
|
|
2337
|
+
Component: Component
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
module.exports = plugin;
|
|
2341
|
+
module.exports.default = plugin;
|
|
2342
|
+
`;
|
|
2343
|
+
files.push({
|
|
2344
|
+
path: `rn-bundle/${pluginName}.bundle.js`,
|
|
2345
|
+
content: rnBundle,
|
|
2346
|
+
});
|
|
2252
2347
|
}
|
|
2253
2348
|
|
|
2254
2349
|
// Emit a manifest.json with the render block the platform requires. A plugin
|
|
@@ -4350,7 +4445,7 @@ const handlers = {
|
|
|
4350
4445
|
verified,
|
|
4351
4446
|
next_steps: [
|
|
4352
4447
|
k === "rn" || k === "adaptive"
|
|
4353
|
-
? `Edit
|
|
4448
|
+
? `Edit rn-src/${plugin_name}.tsx — fill in the Component body, THEN rebuild + commit rn-bundle/${plugin_name}.bundle.js (esbuild command is in the .tsx header). Core does NOT compile the .tsx — mobile loads the committed bundle. A pre-built starter bundle ships with this scaffold, so it renders as-is until you edit it.`
|
|
4354
4449
|
: null,
|
|
4355
4450
|
k === "iframe" || k === "adaptive"
|
|
4356
4451
|
? `Edit ui-dist/${plugin_name}/index.html — replace the placeholder UI.`
|