Skip to content

Commit be23d93

Browse files
committed
[release] Eager prefs probe in select_appwrite_project, slim get_auth_status, drop sessionProjectId
select_appwrite_project now resolves auth at select time instead of leaving the override identity-only. Precedence: 1. caller-supplied apiKey / sessionCookie 2. YAML-inline real apiKey / sessionCookie 3. cached prefsKey from ~/.appwrite/projects.json (probed for freshness) 4. findWorkingSession(endpoint, projectId) — probe all prefs cookies 5. none — selection still binds identity, response surfaces a warning Response gains `authPinned: { method, source, prefsKey }` and a `warning` field that names exactly what was tried when no auth was found. The working prefsKey is persisted to the project registry so future select-by-projectId calls short-circuit straight to the cached cookie. get_auth_status default output collapses to nine slim keys (authenticated, authMethod, endpoint, projectId, projectDir, configSource, source, warnings, resolveError). The previous serverDefaults / cwdProject / sessionOverride / effectiveConfigDir blocks lived at the top level and created a false impression of two projects in play whenever the override projectId differed from the cwd-config projectId. Pass `includeDiagnostics: true` to reattach those blocks under a single `diagnostics` field. sessionProjectId removed from the YAML schema (appwrite-utils + appwrite-utils-helpers) and from ConfigManager's session-loading branch. Was a cache hint written back to user-checked-in YAML files — the role is taken over by the projects.json registry, which is MCP-owned and never touches user-tracked files. Existing YAMLs with the stray field still load cleanly (Zod strips unknown keys) and writeYamlConfig no longer emits the field on rewrite. ProjectRegistryEntry and ProjectOverride gain optional prefsKey. list_appwrite_projects.registeredProjects[] surfaces it.
1 parent a0215b7 commit be23d93

10 files changed

Lines changed: 758 additions & 163 deletions

File tree

packages/appwrite-utils-helpers/src/config/ConfigManager.ts

Lines changed: 7 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,13 @@ export class ConfigManager {
306306
// Session explicitly provided via options
307307
session = options.sessionOverride;
308308
logger.debug("Using session override from options", { prefix: "ConfigManager" });
309-
} else if (options.useSession === true) {
310-
// When session is explicitly requested, only accept a verified working session
309+
} else {
310+
// Probe prefs.json for a working session against this endpoint+project.
311+
// Same call whether `useSession` was explicitly requested or not — we no
312+
// longer write the resolved prefs key back to the YAML as a cache hint,
313+
// so the only difference between modes used to be the cache-write path.
314+
// Cross-run caching now lives in ~/.appwrite/projects.json (managed by
315+
// the MCP's ProjectRegistry), not in user-checked-in config files.
311316
const workingSessionResult = await this.sessionService.findWorkingSession(
312317
config.appwriteEndpoint,
313318
config.appwriteProject
@@ -321,111 +326,6 @@ export class ConfigManager {
321326
`Found and tested working session from project ${workingSessionResult.prefsKey}`,
322327
{ prefix: "ConfigManager", email: workingSessionResult.session.email }
323328
);
324-
325-
// Cache the working session key back to config file
326-
config.sessionProjectId = workingSessionResult.prefsKey;
327-
328-
// Write the updated config back to file to cache the session key
329-
const { writeYamlConfig } = await import('./yamlConfig.js');
330-
try {
331-
await writeYamlConfig(configPath, config);
332-
logger.debug(`Cached session key ${workingSessionResult.prefsKey} to config file`, {
333-
prefix: "ConfigManager"
334-
});
335-
} catch (error) {
336-
logger.warn("Failed to cache session key to config file", {
337-
prefix: "ConfigManager",
338-
error: error instanceof Error ? error.message : String(error)
339-
});
340-
}
341-
}
342-
} else {
343-
// First, try using cached sessionProjectId if available
344-
if (config.sessionProjectId) {
345-
logger.debug(`Attempting to use cached session key: ${config.sessionProjectId}`, {
346-
prefix: "ConfigManager"
347-
});
348-
349-
const prefs = await this.sessionService.loadSessionPrefs();
350-
if (prefs && prefs[config.sessionProjectId]) {
351-
const cachedSessionData = prefs[config.sessionProjectId];
352-
353-
// Verify the cached session still works
354-
const sessionInfo: SessionAuthInfo = {
355-
endpoint: cachedSessionData.endpoint,
356-
projectId: config.appwriteProject,
357-
cookie: cachedSessionData.cookie,
358-
email: cachedSessionData.email,
359-
expiresAt: cachedSessionData.expiresAt
360-
};
361-
362-
const isValid = this.sessionService.isValidSession(sessionInfo);
363-
const isWorking = isValid
364-
? await this.sessionService.isSessionWorking(
365-
config.appwriteEndpoint,
366-
config.appwriteProject,
367-
cachedSessionData.cookie
368-
)
369-
: false;
370-
371-
if (isWorking) {
372-
session = sessionInfo;
373-
sessionPrefsKey = config.sessionProjectId;
374-
logger.info(`Using cached session key: ${config.sessionProjectId}`, {
375-
prefix: "ConfigManager",
376-
email: cachedSessionData.email
377-
});
378-
} else {
379-
logger.debug("Cached session is no longer valid, will search for new session", {
380-
prefix: "ConfigManager"
381-
});
382-
}
383-
} else {
384-
logger.debug("Cached session key not found in prefs.json, will search for new session", {
385-
prefix: "ConfigManager"
386-
});
387-
}
388-
}
389-
390-
// If no cached session or it doesn't work, test all available sessions for this endpoint
391-
if (!session) {
392-
logger.debug("No direct session match, testing available sessions for endpoint", {
393-
prefix: "ConfigManager",
394-
endpoint: config.appwriteEndpoint,
395-
projectId: config.appwriteProject
396-
});
397-
398-
const workingSessionResult = await this.sessionService.findWorkingSession(
399-
config.appwriteEndpoint,
400-
config.appwriteProject
401-
);
402-
403-
if (workingSessionResult) {
404-
session = workingSessionResult.session;
405-
sessionPrefsKey = workingSessionResult.prefsKey;
406-
407-
logger.info(
408-
`Found and tested working session from project ${workingSessionResult.prefsKey}`,
409-
{ prefix: "ConfigManager", email: workingSessionResult.session.email }
410-
);
411-
412-
// Cache the working session key back to config file
413-
config.sessionProjectId = workingSessionResult.prefsKey;
414-
415-
// Write the updated config back to file to cache the session key
416-
const { writeYamlConfig } = await import('./yamlConfig.js');
417-
try {
418-
await writeYamlConfig(configPath, config);
419-
logger.debug(`Cached session key ${workingSessionResult.prefsKey} to config file`, {
420-
prefix: "ConfigManager"
421-
});
422-
} catch (error) {
423-
logger.warn("Failed to cache session key to config file", {
424-
prefix: "ConfigManager",
425-
error: error instanceof Error ? error.message : String(error)
426-
});
427-
}
428-
}
429329
}
430330
}
431331

packages/appwrite-utils-helpers/src/config/yamlConfig.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ const YamlConfigSchema = z.object({
1919
email: z.string().optional(),
2020
expiresAt: z.string().optional(),
2121
}).optional(),
22-
sessionProjectId: z.string().optional(),
2322
}),
2423
logging: z
2524
.object({
@@ -199,7 +198,6 @@ export const convertYamlToAppwriteConfig = (yamlConfig: YamlConfig): AppwriteCon
199198
sessionCookie: yamlConfig.appwrite.sessionCookie,
200199
authMethod: yamlConfig.appwrite.authMethod || "auto",
201200
sessionMetadata: yamlConfig.appwrite.sessionMetadata,
202-
sessionProjectId: yamlConfig.appwrite.sessionProjectId,
203201
apiMode: "auto", // Default to auto-detect for dual API support
204202
appwriteClient: null,
205203
logging: {
@@ -567,7 +565,6 @@ export const writeYamlConfig = async (configPath: string, config: AppwriteConfig
567565
sessionCookie: config.sessionCookie,
568566
authMethod: config.authMethod || "auto",
569567
sessionMetadata: config.sessionMetadata,
570-
sessionProjectId: config.sessionProjectId,
571568
},
572569
logging: {
573570
enabled: config.logging?.enabled || false,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import yaml from "js-yaml";
6+
7+
import { loadYamlConfig, writeYamlConfig } from "../src/config/yamlConfig.js";
8+
9+
/**
10+
* Regression coverage for the `sessionProjectId` removal:
11+
*
12+
* - existing user YAMLs that still carry a `sessionProjectId` field (written
13+
* by an older release of this package) must still parse cleanly — Zod
14+
* strips unknown keys by default, so loading just silently drops it
15+
* - `writeYamlConfig` MUST NOT re-emit the field even if it survived round-trip
16+
* via some other path (we want it gone next time the file is written)
17+
*/
18+
19+
let scratch: string;
20+
21+
beforeEach(async () => {
22+
scratch = await mkdtemp(join(tmpdir(), "appwrite-mcp-sessionhygiene-"));
23+
});
24+
25+
afterEach(async () => {
26+
await rm(scratch, { recursive: true, force: true });
27+
});
28+
29+
const yamlWithStraySessionProjectId = `
30+
appwrite:
31+
endpoint: https://x.test/v1
32+
project: real-project-id
33+
key: standard_realkey
34+
authMethod: auto
35+
sessionProjectId: stale-prefs-key
36+
logging:
37+
enabled: false
38+
level: info
39+
backups:
40+
enabled: true
41+
interval: 3600
42+
retention: 30
43+
cleanup: true
44+
data:
45+
enableMockData: false
46+
documentBucketId: documents
47+
usersCollectionName: Members
48+
importDirectory: importData
49+
appwriteCollections: []
50+
appwriteTables: []
51+
functions: []
52+
sites: []
53+
buckets: []
54+
teams: []
55+
extension:
56+
enabled: false
57+
`;
58+
59+
describe("sessionProjectId hygiene", () => {
60+
test("loadYamlConfig tolerates a stray sessionProjectId without crashing", async () => {
61+
const path = join(scratch, "appwrite-config.yaml");
62+
await writeFile(path, yamlWithStraySessionProjectId, "utf-8");
63+
64+
const config = await loadYamlConfig(path);
65+
expect(config).not.toBeNull();
66+
expect(config!.appwriteProject).toBe("real-project-id");
67+
// Zod-stripped — the field never makes it onto the loaded config.
68+
expect((config as any).sessionProjectId).toBeUndefined();
69+
});
70+
71+
test("writeYamlConfig does NOT emit sessionProjectId even when injected onto the config object", async () => {
72+
const path = join(scratch, "appwrite-config.yaml");
73+
await writeFile(path, yamlWithStraySessionProjectId, "utf-8");
74+
75+
const config = await loadYamlConfig(path);
76+
expect(config).not.toBeNull();
77+
78+
// Simulate an older code path that sneaks the field back in.
79+
(config as any).sessionProjectId = "should-never-be-written";
80+
81+
await writeYamlConfig(path, config!);
82+
const raw = await readFile(path, "utf-8");
83+
const parsed = yaml.load(raw) as { appwrite?: Record<string, unknown> };
84+
expect(parsed.appwrite).toBeDefined();
85+
expect("sessionProjectId" in (parsed.appwrite ?? {})).toBe(false);
86+
});
87+
});

packages/appwrite-utils-mcp/src/auth/AuthResolver.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ export interface ProjectOverride {
9898
* trees without restarting from a different working directory.
9999
*/
100100
projectDir?: string;
101+
/**
102+
* Cache hint: which key in `~/.appwrite/prefs.json` last passed a live
103+
* probe for this project (via `select_appwrite_project`'s eager probe).
104+
* Surfaced for diagnostics; the override's `sessionCookie` is the actual
105+
* auth payload.
106+
*/
107+
prefsKey?: string;
101108
}
102109

103110
/**

packages/appwrite-utils-mcp/src/state/ProjectRegistry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ import { randomBytes } from "node:crypto";
3030
export interface ProjectRegistryEntry {
3131
projectDir: string;
3232
endpoint?: string;
33+
/**
34+
* Key into `~/.appwrite/prefs.json` whose cookie last passed a live probe
35+
* for this project (via `SessionAuthService.findWorkingSession`). Used as a
36+
* cache hint on subsequent selections so the resolver can skip re-probing
37+
* every cookie. Stale entries are detected at use time and quietly
38+
* superseded — losing freshness here is never fatal.
39+
*/
40+
prefsKey?: string;
3341
lastSelectedAt: string;
3442
}
3543

@@ -111,6 +119,7 @@ export class ProjectRegistry {
111119
const next: ProjectRegistryEntry = {
112120
projectDir: entry.projectDir,
113121
endpoint: entry.endpoint,
122+
prefsKey: entry.prefsKey,
114123
lastSelectedAt: entry.lastSelectedAt ?? new Date().toISOString(),
115124
};
116125
snapshot[projectId] = next;
@@ -180,13 +189,16 @@ function normalize(parsed: unknown): ProjectRegistrySnapshot {
180189
const entry = raw as {
181190
projectDir?: unknown;
182191
endpoint?: unknown;
192+
prefsKey?: unknown;
183193
lastSelectedAt?: unknown;
184194
};
185195
if (typeof entry.projectDir !== "string" || !entry.projectDir) continue;
186196
out[pid] = {
187197
projectDir: entry.projectDir,
188198
endpoint:
189199
typeof entry.endpoint === "string" && entry.endpoint ? entry.endpoint : undefined,
200+
prefsKey:
201+
typeof entry.prefsKey === "string" && entry.prefsKey ? entry.prefsKey : undefined,
190202
lastSelectedAt:
191203
typeof entry.lastSelectedAt === "string" && entry.lastSelectedAt
192204
? entry.lastSelectedAt

0 commit comments

Comments
 (0)