drafted 1.11.17 → 1.11.19
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/mcp/server.mjs +58 -19
- package/package.json +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -766,6 +766,23 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
|
|
|
766
766
|
const scopedPath = pid ? `${path}${sep}projectId=${pid}` : path;
|
|
767
767
|
const url = `${getServerUrl()}${scopedPath}`;
|
|
768
768
|
const headers = { ...getAuthHeaders(), ...extraHeaders };
|
|
769
|
+
// Bind every data request to the org this MCP session is actually working in
|
|
770
|
+
// (the active project's org, or the org chosen via get_org switch). Without
|
|
771
|
+
// this, project-scoped routes that resolve org from the SHARED server-side
|
|
772
|
+
// session row (notably /api/fs ls + frame read, via effectiveOrgId) break when
|
|
773
|
+
// a concurrent agent session for the same user flips that row to another org:
|
|
774
|
+
// the active project then "isn't in the current org", the 404 handler below
|
|
775
|
+
// clears it, and the agent sees the "active project no longer in the current
|
|
776
|
+
// org" churn. X-Drafted-Org is validated against the caller's memberships
|
|
777
|
+
// server-side and never mutates the session, so it scopes resolution
|
|
778
|
+
// per-request — exactly like the org-less /project/:slug link that already
|
|
779
|
+
// works. An explicit override in extraHeaders (e.g. the wiki/skill `org` arg)
|
|
780
|
+
// always wins, and /auth/* is left untouched so it reports true session state.
|
|
781
|
+
const boundOrg = getSessionState().boundOrgId;
|
|
782
|
+
const hasExplicitOrg = Object.keys(headers).some((k) => k.toLowerCase() === 'x-drafted-org');
|
|
783
|
+
if (boundOrg && !hasExplicitOrg && !path.startsWith('/auth/')) {
|
|
784
|
+
headers['X-Drafted-Org'] = boundOrg;
|
|
785
|
+
}
|
|
769
786
|
const opts = { method, headers };
|
|
770
787
|
|
|
771
788
|
if (body !== undefined) {
|
|
@@ -964,25 +981,6 @@ function setMcpActiveProject(projectId, meta = null) {
|
|
|
964
981
|
if (meta?.orgId) sess.boundOrgId = meta.orgId;
|
|
965
982
|
}
|
|
966
983
|
|
|
967
|
-
// Clear the active project if its orgId no longer matches the current org.
|
|
968
|
-
// Catches the silent drift where org switches (via get_org switch, browser
|
|
969
|
-
// tab, parallel agent, etc.) leave the MCP session pointing at a project
|
|
970
|
-
// the API can no longer see — which surfaces as confusing "Project not
|
|
971
|
-
// found" errors on otherwise-valid tool calls.
|
|
972
|
-
async function ensureProjectMatchesOrg() {
|
|
973
|
-
const s = getState();
|
|
974
|
-
const meta = s.projectMeta;
|
|
975
|
-
if (!s.projectId || !meta?.orgId) return;
|
|
976
|
-
const currentOrgId = await getCurrentOrgId();
|
|
977
|
-
if (currentOrgId && currentOrgId !== meta.orgId) {
|
|
978
|
-
s.projectId = null;
|
|
979
|
-
s.projectMeta = null;
|
|
980
|
-
const sess = getSessionState();
|
|
981
|
-
sess.activeProjectId = null;
|
|
982
|
-
sess.activeProjectMeta = null;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
984
|
// Returns { id, slug, name, orgId } for the project this MCP session most
|
|
987
985
|
// recently opened — what frame mutations will actually target. Echoed on
|
|
988
986
|
// every mutation so silent cross-project drift is visible.
|
|
@@ -1084,6 +1082,36 @@ async function getCurrentOrgContext() {
|
|
|
1084
1082
|
} catch { return null; }
|
|
1085
1083
|
}
|
|
1086
1084
|
|
|
1085
|
+
// Project-less mutations (wiki, skills) have no resource to derive the org from.
|
|
1086
|
+
// If the agent never bound an org this session (no project opened, no get_org
|
|
1087
|
+
// switch) and passed no explicit org, refuse to GUESS when the user belongs to
|
|
1088
|
+
// more than one org — otherwise the write silently lands in whatever org the
|
|
1089
|
+
// session happened to inherit (the "wiki write into the wrong org" failure).
|
|
1090
|
+
// Single-org users are unambiguous and proceed untouched. (DRAFT-36 Phase 4.)
|
|
1091
|
+
async function requireBoundOrgForProjectlessMutation(explicitOrg) {
|
|
1092
|
+
if (explicitOrg) return;
|
|
1093
|
+
const sess = getSessionState();
|
|
1094
|
+
if (sess.boundOrgId) return; // bound via project open / get_org switch
|
|
1095
|
+
if (getState().projectId) return; // an active project implies its org
|
|
1096
|
+
let count = sess.cachedOrgCount;
|
|
1097
|
+
if (count == null) {
|
|
1098
|
+
try {
|
|
1099
|
+
const d = await api('GET', '/api/orgs');
|
|
1100
|
+
count = (d.orgs || d || []).length;
|
|
1101
|
+
sess.cachedOrgCount = count;
|
|
1102
|
+
} catch { return; } // can't determine membership — don't block a legit write
|
|
1103
|
+
}
|
|
1104
|
+
if (count > 1) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`Refusing to guess the org: no project is open and no org was bound this ` +
|
|
1107
|
+
`session, but you belong to ${count} orgs — a project-less wiki/skill write ` +
|
|
1108
|
+
`would land in whichever org the session inherited. Call ` +
|
|
1109
|
+
`get_org(action="switch", orgId=...) or project(action="open") first ` +
|
|
1110
|
+
`(skill calls may also pass org=...).`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1087
1115
|
async function getOrgSkills(orgId) {
|
|
1088
1116
|
if (!orgId) return [];
|
|
1089
1117
|
// No cache: gate freshness > the ~5ms HTTP roundtrip. Otherwise an attach
|
|
@@ -2869,6 +2897,11 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2869
2897
|
}, async (args) => {
|
|
2870
2898
|
try {
|
|
2871
2899
|
const { action } = args;
|
|
2900
|
+
// Org-creating/editing skill actions on the project-less surface must have a
|
|
2901
|
+
// bound org (or explicit org=) when the user is multi-org. (DRAFT-36 Phase 4.)
|
|
2902
|
+
if (['add', 'update', 'remove', 'push', 'fork'].includes(action)) {
|
|
2903
|
+
await requireBoundOrgForProjectlessMutation(args.org);
|
|
2904
|
+
}
|
|
2872
2905
|
switch (action) {
|
|
2873
2906
|
case 'search': {
|
|
2874
2907
|
const { query, tags, scope = 'all', limit, offset, compact } = args;
|
|
@@ -3103,6 +3136,12 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3103
3136
|
try {
|
|
3104
3137
|
const { action } = args;
|
|
3105
3138
|
|
|
3139
|
+
// Project-less wiki mutations must have a bound org when the user is
|
|
3140
|
+
// multi-org, so a write never silently lands in an inherited org. (Ph4.)
|
|
3141
|
+
if (['write', 'edit', 'mv', 'rm', 'bulk-write'].includes(action)) {
|
|
3142
|
+
await requireBoundOrgForProjectlessMutation(undefined);
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3106
3145
|
// Resolve org context once for the wiki tool. Each MCP session is bound
|
|
3107
3146
|
// to one org (parallel agents on different orgs is supported by design).
|
|
3108
3147
|
const orgCtx = await getCurrentOrgContext();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.19",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|