tascan-mcp 3.16.1 → 3.16.2
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 +3 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/scopes.cjs +41 -9
- package/tools.cjs +73 -0
package/README.md
CHANGED
|
@@ -288,6 +288,9 @@ API keys are scoped to your organization and support rate limiting (60 requests/
|
|
|
288
288
|
|
|
289
289
|
## Changelog
|
|
290
290
|
|
|
291
|
+
### v3.16.2 — 2026-09-28
|
|
292
|
+
- 98 tools. Rolls up 3.16.0 to 3.16.2: `tascan_get_usage` (GET /usage, per-key daily actions and rate-limit headers, migration 220); the device control plane read/revoke tools (`tascan_list_devices`, `tascan_get_device`, `tascan_revoke_device`; register and rotate stay admin-app only by design); `tascan_delegate_to_agent` (child keys with a TTL of 60 s to 24 h clamped to the parent, non-delegable flags refused, the plaintext returned once) and the per-key action budgets that fail closed (migration 221). Hosted MCP at https://app.tascan.io/mcp already serves this version; this release brings the stdio package up to it.
|
|
293
|
+
|
|
291
294
|
### v3.15.0 — 2026-09-19
|
|
292
295
|
|
|
293
296
|
- `tascan_create_cycle` gains an optional `reviews[]` array — a multi-lens review panel (design item 14a): 1–8 lenses, each `{lens, brief, provider: openai|anthropic|gemini, model?, blocking? (default true), max_tool_calls? (openai only)}`, forwarded to `POST /coord/cycles`; at least one lens must be blocking. Omit it for the single OpenAI review.
|
package/index.js
CHANGED
|
@@ -53,7 +53,7 @@ async function api(method, path, body, idemKey) {
|
|
|
53
53
|
api.key = API_KEY; // raw-fetch tools read the key from here (S99: those functions now require auth)
|
|
54
54
|
|
|
55
55
|
const server = new Server(
|
|
56
|
-
{ name: 'tascan', version: '3.16.
|
|
56
|
+
{ name: 'tascan', version: '3.16.2' },
|
|
57
57
|
{ capabilities: { tools: {} } }
|
|
58
58
|
);
|
|
59
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tascan-mcp",
|
|
3
|
-
"version": "3.16.
|
|
3
|
+
"version": "3.16.2",
|
|
4
4
|
"mcpName": "io.github.snowbikemike/tascan-mcp",
|
|
5
5
|
"description": "TaScan MCP Server — Closed-loop autonomous operations protocol. 70 tools for projects, tasks, workers, QR codes, NFC tags, worker marketplace with consented SMS invites, geofenced zones with rules (work site / hazard + AI-verified PPE checkpoints / containment / restricted, breach alerts, zone compliance audit), AI condition assessment, worker passports, verification-gated gig payments, client invoicing from verified work (overtime / day rate / per diem / expenses), shareable report links (completion, client service report, project, evidence pack), signed Action Receipts (Ed25519 JWS), cross-entity search, duplicate-worker detection, worker identity merge, AI issue analysis, and autonomous remediation dispatch. 15 patents filed (470+ claims). Task. Scan. Done.",
|
|
6
6
|
"type": "module",
|
package/scopes.cjs
CHANGED
|
@@ -34,7 +34,13 @@ const DISPATCH_CODE = 'agent:dispatch:code';
|
|
|
34
34
|
// requires full, and unreachable through OAuth (mcp-oauth.js's consent flow passes no device flag, so clamp()
|
|
35
35
|
// below never grants it to a connector token even though clamp is capable of granting it to an owner).
|
|
36
36
|
const DEVICE_ADMIN = 'device:admin';
|
|
37
|
-
|
|
37
|
+
// B2 (migration 221): delegate — a FOURTH additive permission, never implied by a tier — owner-only, requires
|
|
38
|
+
// full (like device:admin), and unreachable through OAuth (mcp-oauth.js's consent flow passes no delegate flag,
|
|
39
|
+
// so clamp() below never grants it to a connector token even though clamp is capable of granting it to an owner).
|
|
40
|
+
// A key holding it may mint time-limited, narrower child keys (coord_delegate_key) — read/write only, never a
|
|
41
|
+
// wider or lateral grant (I-6).
|
|
42
|
+
const DELEGATE = 'delegate';
|
|
43
|
+
const SCOPE_VALUES = ['read', 'write', 'full', DISPATCH, DISPATCH_CODE, DEVICE_ADMIN, DELEGATE];
|
|
38
44
|
const ALL_SCOPES = SCOPE_VALUES.slice();
|
|
39
45
|
|
|
40
46
|
// Seed agent inbox (tools.cjs AGENT_REGISTRY, agent-execute, agent.js, migrations 046/137/151).
|
|
@@ -74,23 +80,25 @@ function dispatchKind(titles) {
|
|
|
74
80
|
// (P0 / migration 216): a legacy row minted before this permission existed must not silently gain it.
|
|
75
81
|
// null / undefined / [] → read, no dispatch, no device_admin (FAIL CLOSED)
|
|
76
82
|
function normalize(v) {
|
|
77
|
-
if (v == null) return { tier: 'read', dispatch: false, code: false, device_admin: false, legacy: false };
|
|
83
|
+
if (v == null) return { tier: 'read', dispatch: false, code: false, device_admin: false, delegate: false, legacy: false };
|
|
78
84
|
const a = Array.isArray(v) ? v.map(String) : String(v).split(/[\s+,]+/).filter(Boolean);
|
|
79
|
-
if (a.includes('*')) return { tier: 'full', dispatch: true, code: true, device_admin: true, legacy: true };
|
|
80
|
-
if (a.includes('all')) return { tier: 'full', dispatch: true, code: true, device_admin: false, legacy: true };
|
|
85
|
+
if (a.includes('*')) return { tier: 'full', dispatch: true, code: true, device_admin: true, delegate: true, legacy: true };
|
|
86
|
+
if (a.includes('all')) return { tier: 'full', dispatch: true, code: true, device_admin: false, delegate: false, legacy: true };
|
|
81
87
|
let tier = 'read';
|
|
82
88
|
for (const t of TIERS) if (a.includes(t) && RANK[t] > RANK[tier]) tier = t;
|
|
83
89
|
const dispatch = a.includes(DISPATCH);
|
|
84
90
|
// code without dispatch is not a state the consent page or key UI can produce; treat as dispatch too
|
|
85
91
|
const code = a.includes(DISPATCH_CODE);
|
|
86
92
|
const deviceAdmin = a.includes(DEVICE_ADMIN);
|
|
87
|
-
|
|
93
|
+
const delegate = a.includes(DELEGATE);
|
|
94
|
+
return { tier, dispatch: dispatch || code, code, device_admin: deviceAdmin, delegate, legacy: false };
|
|
88
95
|
}
|
|
89
96
|
function toArray(n) {
|
|
90
97
|
const out = TIERS.slice(0, RANK[n.tier] || 1);
|
|
91
98
|
if (n.dispatch || n.code) out.push(DISPATCH);
|
|
92
99
|
if (n.code) out.push(DISPATCH_CODE);
|
|
93
100
|
if (n.device_admin) out.push(DEVICE_ADMIN);
|
|
101
|
+
if (n.delegate) out.push(DELEGATE);
|
|
94
102
|
return out;
|
|
95
103
|
}
|
|
96
104
|
function toScopeString(n) { return toArray(n).join(' '); }
|
|
@@ -133,7 +141,7 @@ function echoScope(str) {
|
|
|
133
141
|
// P0 (migration 216): device_admin is accepted here for completeness and unit-tested (a non-owner clamp drops
|
|
134
142
|
// it), but the ONLY real caller — mcp-oauth.js's OAuth consent flow — never passes a device flag in `pick` at
|
|
135
143
|
// all, so a connector token can never hold device:admin regardless of the granting admin's role.
|
|
136
|
-
function clamp({ tier, dispatch, code, device_admin }, role) {
|
|
144
|
+
function clamp({ tier, dispatch, code, device_admin, delegate }, role) {
|
|
137
145
|
const t = TIERS.includes(tier) ? tier : 'read';
|
|
138
146
|
const owner = role === 'owner';
|
|
139
147
|
const clamped = [];
|
|
@@ -144,7 +152,11 @@ function clamp({ tier, dispatch, code, device_admin }, role) {
|
|
|
144
152
|
else if (!!code && !gc) clamped.push(DISPATCH_CODE); // ticked code without dispatch
|
|
145
153
|
let gda = !!device_admin && owner;
|
|
146
154
|
if (!!device_admin && !gda) clamped.push(DEVICE_ADMIN);
|
|
147
|
-
|
|
155
|
+
// B2 (migration 221): delegate is owner-only AND requires the final (already-clamped) tier to be full — the
|
|
156
|
+
// same "only an owner-held, full-tier key may ever mint a delegation" rule 221's own CHECK enforces in storage.
|
|
157
|
+
let gdel = !!delegate && owner && gt === 'full';
|
|
158
|
+
if (!!delegate && !gdel) clamped.push(DELEGATE);
|
|
159
|
+
return { granted: { tier: gt, dispatch: gd, code: gc, device_admin: gda, delegate: gdel, legacy: false }, clamped };
|
|
148
160
|
}
|
|
149
161
|
|
|
150
162
|
// POST /api/v1/keys body.scopes → canonical array or null (rejects 'all', '*', junk, empty,
|
|
@@ -154,6 +166,7 @@ function validateStored(arr) {
|
|
|
154
166
|
if (!arr.every(s => typeof s === 'string' && SCOPE_VALUES.includes(s))) return null;
|
|
155
167
|
if (arr.includes(DISPATCH_CODE) && !arr.includes(DISPATCH)) return null;
|
|
156
168
|
if (arr.includes(DEVICE_ADMIN) && !arr.includes('full')) return null;
|
|
169
|
+
if (arr.includes(DELEGATE) && !arr.includes('full')) return null;
|
|
157
170
|
return toArray(normalize(arr));
|
|
158
171
|
}
|
|
159
172
|
|
|
@@ -171,6 +184,7 @@ function check(granted, need) {
|
|
|
171
184
|
if (need.human_only) return { code: 'human_session_required', required: 'human_session_required', granted_scope: toScopeString(g) };
|
|
172
185
|
if ((RANK[g.tier] || 0) < (RANK[need.tier] || 99)) return deny(need.tier);
|
|
173
186
|
if (need.device_admin && !g.device_admin) return deny(DEVICE_ADMIN);
|
|
187
|
+
if (need.delegate && !g.delegate) return deny(DELEGATE);
|
|
174
188
|
if (need.dispatch === 'code' && !g.code) return deny(DISPATCH_CODE);
|
|
175
189
|
if (need.dispatch === true && !g.dispatch) return deny(DISPATCH);
|
|
176
190
|
return null;
|
|
@@ -250,7 +264,14 @@ const TOOL_SCOPES = {
|
|
|
250
264
|
tascan_list_devices: R, tascan_get_device: R,
|
|
251
265
|
tascan_revoke_device: { tier: 'full', device_admin: true },
|
|
252
266
|
// B1 (migration 220): GET /usage wrapped as a tool — read tier, same as every other read-only usage/analytics tool.
|
|
253
|
-
tascan_get_usage: R
|
|
267
|
+
tascan_get_usage: R,
|
|
268
|
+
// B2 (migration 221): per-key budgets + delegated child keys. tascan_get_budget is the caller's own row (read
|
|
269
|
+
// tier, same idiom as tascan_get_usage). tascan_delegate_to_agent mints (NOT destructiveHint — see tools.cjs);
|
|
270
|
+
// tascan_revoke_delegation is destructive+idempotent. Both need write + delegate (owner-only to grant, I-6).
|
|
271
|
+
tascan_get_budget: R,
|
|
272
|
+
tascan_delegate_to_agent: { tier: 'write', delegate: true },
|
|
273
|
+
tascan_list_delegations: R,
|
|
274
|
+
tascan_revoke_delegation: { tier: 'write', delegate: true }
|
|
254
275
|
};
|
|
255
276
|
// Tools classified here that do not yet exist in tools.cjs (other workstreams' patches). Empty since
|
|
256
277
|
// tascan_get_receipt landed in tools.cjs (S100 phase 2b); harnesses assert TOOL_SCOPES ⊇ tools.cjs.
|
|
@@ -352,6 +373,16 @@ const ROUTE_SCOPES = {
|
|
|
352
373
|
// V9b (204): GET/PUT /evidence/policy/:task_id — read a task's evidence-policy progress / author + pin one.
|
|
353
374
|
evidence_policy: { GET: R, PUT: W }, // GET /builds/:build_ref/file?path=&offset=&limit= ≤ 12,000 chars per call
|
|
354
375
|
keys: 'admin', key: 'admin',
|
|
376
|
+
// B2 (migration 221): key_budget (PUT/GET /keys/:id/budget) joins the admin-JWT-only idiom (api-v1.js's
|
|
377
|
+
// DECISION_ASSERTION_ADMIN_HANDLERS-style set) — a tsk_/service bearer is refused 403 human_session_required
|
|
378
|
+
// before this table is ever consulted (I-3: no key, ever, sets or reads ANOTHER key's budget); the whole entry
|
|
379
|
+
// is the 'admin' marker, same as keys/key above, since the handler self-gates (owner-only for PUT, any admin
|
|
380
|
+
// role for GET). budget (GET /budget, no :id) is the CALLING key's own row — an ordinary read-tier key route.
|
|
381
|
+
// delegations/delegation need write + delegate (owner-only to grant, I-6).
|
|
382
|
+
key_budget: 'admin',
|
|
383
|
+
budget: { GET: R },
|
|
384
|
+
delegations: { POST: { tier: 'write', delegate: true }, GET: R },
|
|
385
|
+
delegation: { DELETE: { tier: 'write', delegate: true } },
|
|
355
386
|
// H1 (migration 215): human decision binding — admin-JWT-only, same gate-skip idiom as keys/key (api-v1.js's
|
|
356
387
|
// DECISION_ASSERTION_ADMIN_HANDLERS uses authenticateAdminSession and refuses a tsk_/service key 403 human_session_required).
|
|
357
388
|
decision_assertion_options: 'admin', decision_assertions: 'admin', decision_assertion: 'admin',
|
|
@@ -387,6 +418,7 @@ const SCOPE_LABEL = {
|
|
|
387
418
|
[DISPATCH]: 'the "agent:dispatch" permission ("Let this app hand tasks to my AI agents")',
|
|
388
419
|
[DISPATCH_CODE]: 'the "agent:dispatch:code" permission ("...including code and shell tasks on my computer")',
|
|
389
420
|
[DEVICE_ADMIN]: 'the "device:admin" permission (register/rotate/revoke devices) — owner-only, requires full, and not reachable through OAuth',
|
|
421
|
+
[DELEGATE]: 'the "delegate" permission (mint time-limited child API keys) — owner-only, requires full, and not reachable through OAuth',
|
|
390
422
|
unclassified: 'a classification (this tool is not in the scope map)'
|
|
391
423
|
};
|
|
392
424
|
function reconnectHint(required) {
|
|
@@ -415,7 +447,7 @@ function restDenialHeaders(d) {
|
|
|
415
447
|
}
|
|
416
448
|
|
|
417
449
|
module.exports = {
|
|
418
|
-
SCOPE_VALUES, ALL_SCOPES, TIERS, RANK, DISPATCH, DISPATCH_CODE, DEVICE_ADMIN, AGENT_INBOX_IDS,
|
|
450
|
+
SCOPE_VALUES, ALL_SCOPES, TIERS, RANK, DISPATCH, DISPATCH_CODE, DEVICE_ADMIN, DELEGATE, AGENT_INBOX_IDS,
|
|
419
451
|
TASK_TYPES, LOCAL_TASK_TYPES, CLOUD_TASK_TYPES, REVIEWER_TASK_TYPES, LOCAL_ONLY_TASK_TYPES, CODE_TASK_TYPES, taskType, dispatchScopeFor, dispatchKind,
|
|
420
452
|
normalize, toArray, toScopeString, parseRequested, echoScope, SCOPE_PARAM_MAX, clamp, validateStored, check,
|
|
421
453
|
TOOL_SCOPES, PENDING_TOOLS, ROUTE_SCOPES, requiredForTool, requiredForRoute,
|
package/tools.cjs
CHANGED
|
@@ -2750,6 +2750,79 @@ const TOOLS = [
|
|
|
2750
2750
|
return lines.join('\n');
|
|
2751
2751
|
}
|
|
2752
2752
|
},
|
|
2753
|
+
// B2 (migration 221): per-key action budgets (#10, I-3) + delegated, time-limited child keys (#11, I-6, I-10).
|
|
2754
|
+
{
|
|
2755
|
+
name: 'tascan_get_budget',
|
|
2756
|
+
description: 'Read THIS credential\'s own per-key action budget (GET /budget): daily limits (sms, invites, payment-request cents per day and per request, dispatch, human pages) and today\'s spend against them. A NULL limit (or no budget row at all) means unlimited on that counter. A key can only ever see its OWN budget, never another key\'s (I-3, no tool or key ever reads another key\'s row). Read tier.',
|
|
2757
|
+
inputSchema: { type: 'object', properties: {} },
|
|
2758
|
+
annotations: { title: 'Get Budget', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2759
|
+
handler: async (args, api) => {
|
|
2760
|
+
const result = await api('GET', '/budget');
|
|
2761
|
+
const d = result.data || {};
|
|
2762
|
+
const b = d.budget || {}, u = d.usage || {};
|
|
2763
|
+
const rows = [
|
|
2764
|
+
['sms_per_day', 'sms_count', 'sms/day'], ['invites_per_day', 'invites_count', 'invites/day'],
|
|
2765
|
+
['payment_request_cents_per_day', 'payment_request_cents_count', 'payment request cents/day'],
|
|
2766
|
+
['payment_request_cents_max', null, 'payment request cents/request (max)'],
|
|
2767
|
+
['dispatch_per_day', 'dispatch_count', 'dispatch/day'], ['human_pages_per_day', 'human_pages_count', 'human pages/day']
|
|
2768
|
+
];
|
|
2769
|
+
const lines = ["this key's budget (null = unlimited):"];
|
|
2770
|
+
for (const [limitKey, usageKey, label] of rows) {
|
|
2771
|
+
const limit = b[limitKey] != null ? b[limitKey] : 'unlimited';
|
|
2772
|
+
const used = usageKey ? (u[usageKey] != null ? u[usageKey] : 0) : null;
|
|
2773
|
+
lines.push(` ${label}: ${used != null ? used + ' / ' : ''}${limit}`);
|
|
2774
|
+
}
|
|
2775
|
+
return lines.join('\n') + '\n\n' + JSON.stringify(d, null, 2);
|
|
2776
|
+
}
|
|
2777
|
+
},
|
|
2778
|
+
{
|
|
2779
|
+
name: 'tascan_delegate_to_agent',
|
|
2780
|
+
description: 'Mint a time-limited, NARROWER child API key (POST /delegations, roadmap #11) — only read/write scopes may be requested; full, agent:dispatch, agent:dispatch:code, device:admin, delegate and webhooks:manage are always refused by name (I-6). TTL defaults to 1 hour (3600s), clamps to [60 seconds, 24 hours], and clamps further to THIS key\'s own expiry if it has one — a delegated key can never outlive its grantor. The child\'s tier is always a SUBSET of this key\'s own effective tier, never wider or lateral. Requires the delegate permission (owner-only to grant, requires full). The returned raw key is shown to you exactly ONCE — it is never stored server-side and cannot be retrieved again (I-10). Revoking this key (or any ancestor) cascades immediately to every key it delegated.',
|
|
2781
|
+
inputSchema: {
|
|
2782
|
+
type: 'object',
|
|
2783
|
+
properties: {
|
|
2784
|
+
scopes: { type: 'array', items: { type: 'string', enum: ['read', 'write'] }, description: 'Scopes to request for the child key — read and/or write only.' },
|
|
2785
|
+
ttl_seconds: { type: 'number', description: 'How long the child key lives, in seconds (default 3600 = 1h; clamped to [60, 86400] and to this key\'s own expiry).' },
|
|
2786
|
+
resource_constraints: { type: 'object', description: 'Optional free-form narrowing (e.g. project/list ids) recorded on the child key as metadata.' }
|
|
2787
|
+
},
|
|
2788
|
+
required: ['scopes']
|
|
2789
|
+
},
|
|
2790
|
+
annotations: { title: 'Delegate To Agent', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
2791
|
+
handler: async (args, api) => {
|
|
2792
|
+
if (!Array.isArray(args.scopes) || !args.scopes.length) throw new Error('scopes is required (array of "read"/"write")');
|
|
2793
|
+
const result = await api('POST', '/delegations', { scopes: args.scopes, ttl_seconds: args.ttl_seconds, resource_constraints: args.resource_constraints });
|
|
2794
|
+
const d = result.data || {};
|
|
2795
|
+
return `Delegated key ${d.key_id} minted, scopes [${(d.scopes || []).join(', ')}], expires ${d.expires_at}.\nSAVE THIS KEY NOW — it will not be shown again:\n${d.key}`;
|
|
2796
|
+
}
|
|
2797
|
+
},
|
|
2798
|
+
{
|
|
2799
|
+
name: 'tascan_list_delegations',
|
|
2800
|
+
description: 'List delegated child keys (GET /delegations): this key\'s own direct children (id, label, scopes, active/revoked, depth, expires_at) — never the raw credential. An admin session sees every delegated key in the org. Read tier.',
|
|
2801
|
+
inputSchema: { type: 'object', properties: {} },
|
|
2802
|
+
annotations: { title: 'List Delegations', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2803
|
+
handler: async (args, api) => {
|
|
2804
|
+
const result = await api('GET', '/delegations');
|
|
2805
|
+
const data = result.data || [];
|
|
2806
|
+
const out = `${data.length} delegated key(s)\n\n${JSON.stringify(data, null, 2)}`;
|
|
2807
|
+
return out.length > 12000 ? out.slice(0, 12000) + '\n…[truncated at 12000 chars]' : out;
|
|
2808
|
+
}
|
|
2809
|
+
},
|
|
2810
|
+
{
|
|
2811
|
+
name: 'tascan_revoke_delegation',
|
|
2812
|
+
description: 'Revoke a delegated child key immediately (DELETE /delegations/:key_id) — self-revoke, revoke by an ancestor key, or by an admin session of the org; any other caller is refused. Revoking cascades to every key IT delegated, in turn (the database trigger, not application code). Idempotent: revoking an already-revoked key reports already_revoked, no error.',
|
|
2813
|
+
inputSchema: {
|
|
2814
|
+
type: 'object',
|
|
2815
|
+
properties: { key_id: { type: 'string', description: 'The delegated key id to revoke (from tascan_list_delegations)' } },
|
|
2816
|
+
required: ['key_id']
|
|
2817
|
+
},
|
|
2818
|
+
annotations: { title: 'Revoke Delegation', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
2819
|
+
handler: async (args, api) => {
|
|
2820
|
+
if (!args.key_id) throw new Error('key_id is required');
|
|
2821
|
+
const result = await api('DELETE', `/delegations/${args.key_id}`);
|
|
2822
|
+
const d = result.data || {};
|
|
2823
|
+
return d.already_revoked ? `Key ${args.key_id} was already revoked.` : `Key ${args.key_id} revoked.`;
|
|
2824
|
+
}
|
|
2825
|
+
},
|
|
2753
2826
|
// P0 (migration 216): device control plane — split out (194 KB already close to the ~200 KB bundle cap).
|
|
2754
2827
|
...require('./tools-devices.cjs')
|
|
2755
2828
|
];
|