@kynver-app/mcp-agent-os 0.3.18 → 0.3.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.
@@ -0,0 +1,29 @@
1
+ import { type IncomingMessage } from "node:http";
2
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ export declare const MAX_SSE_BODY_BYTES: number;
4
+ export type SessionAuth = {
5
+ kind: "apiKey";
6
+ apiKey: string;
7
+ } | {
8
+ kind: "internal";
9
+ userId: string;
10
+ };
11
+ type RequestContextStore = {
12
+ run: <T>(store: {
13
+ userId?: string;
14
+ }, fn: () => Promise<T>) => Promise<T>;
15
+ };
16
+ export type StartKynverMcpSseServerOptions = {
17
+ label: string;
18
+ defaultPort: number;
19
+ createMcpServer: () => McpServer | Promise<McpServer>;
20
+ requestContext: RequestContextStore;
21
+ };
22
+ export declare function headerValue(req: IncomingMessage, name: string): string | null;
23
+ export declare function timingSafeEqualString(a: string, b: string): boolean;
24
+ export declare function hasForbiddenQueryAuth(url: URL): boolean;
25
+ export declare function resolveListenHost(): string;
26
+ export declare function parseJsonBody(req: IncomingMessage, maxBytes?: number): Promise<unknown>;
27
+ export declare function validateMcporterServerName(name: string): string;
28
+ export declare function startKynverMcpSseServer(options: StartKynverMcpSseServerOptions): void;
29
+ export {};
@@ -0,0 +1,191 @@
1
+ // @generated — synced from packages/kynver-mcp-sse-lib/src/index.ts via scripts/sync-mcp-sse-server.mjs
2
+ // Do not edit by hand; change the canonical file and re-run the sync script.
3
+ import { createServer } from "node:http";
4
+ import { timingSafeEqual } from "node:crypto";
5
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6
+ export const MAX_SSE_BODY_BYTES = 1 * 1024 * 1024;
7
+ const FORBIDDEN_QUERY_KEYS = ["secret", "userId"];
8
+ export function headerValue(req, name) {
9
+ const v = req.headers[name];
10
+ return typeof v === "string" ? v : Array.isArray(v) ? (v[0] ?? null) : null;
11
+ }
12
+ export function timingSafeEqualString(a, b) {
13
+ const left = Buffer.from(a);
14
+ const right = Buffer.from(b);
15
+ if (left.length !== right.length)
16
+ return false;
17
+ return timingSafeEqual(left, right);
18
+ }
19
+ export function hasForbiddenQueryAuth(url) {
20
+ return FORBIDDEN_QUERY_KEYS.some((key) => url.searchParams.has(key));
21
+ }
22
+ function queryApiKey(url) {
23
+ return url.searchParams.get("apiKey")?.trim() ?? "";
24
+ }
25
+ export function resolveListenHost() {
26
+ const configured = process.env.MCP_SSE_HOST?.trim();
27
+ return configured || "127.0.0.1";
28
+ }
29
+ export function parseJsonBody(req, maxBytes = MAX_SSE_BODY_BYTES) {
30
+ return new Promise((resolve, reject) => {
31
+ const chunks = [];
32
+ let totalSize = 0;
33
+ req.on("data", (chunk) => {
34
+ totalSize += chunk.length;
35
+ if (totalSize > maxBytes) {
36
+ req.destroy();
37
+ reject(new Error("Request body too large"));
38
+ return;
39
+ }
40
+ chunks.push(chunk);
41
+ });
42
+ req.on("end", () => {
43
+ try {
44
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
45
+ }
46
+ catch (error) {
47
+ reject(error);
48
+ }
49
+ });
50
+ req.on("error", reject);
51
+ });
52
+ }
53
+ export function validateMcporterServerName(name) {
54
+ const trimmed = name.trim();
55
+ if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
56
+ throw new Error("Invalid mcporter server name.");
57
+ }
58
+ return trimmed;
59
+ }
60
+ function bearerToken(req) {
61
+ const auth = headerValue(req, "authorization");
62
+ if (!auth)
63
+ return "";
64
+ const match = /^Bearer\s+(.+)$/i.exec(auth);
65
+ return match?.[1]?.trim() ?? "";
66
+ }
67
+ function resolveInternalAuth(req, serviceSecret) {
68
+ const internalUserId = headerValue(req, "x-kynver-user-id");
69
+ const internalSecret = headerValue(req, "x-kynver-service-secret");
70
+ if (!internalUserId || !serviceSecret || !internalSecret)
71
+ return null;
72
+ if (!timingSafeEqualString(internalSecret, serviceSecret))
73
+ return null;
74
+ return { userId: internalUserId, auth: { kind: "internal", userId: internalUserId } };
75
+ }
76
+ function verifyPostAuth(req, url, sessionAuth, serviceSecret) {
77
+ if (sessionAuth.kind === "apiKey") {
78
+ const token = bearerToken(req) || queryApiKey(url);
79
+ return token.length > 0 && timingSafeEqualString(token, sessionAuth.apiKey);
80
+ }
81
+ const internal = resolveInternalAuth(req, serviceSecret);
82
+ return (internal !== null &&
83
+ internal.userId === sessionAuth.userId &&
84
+ internal.auth.kind === "internal");
85
+ }
86
+ async function resolveUserFromApiKey(baseUrl, apiKey) {
87
+ try {
88
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/users/me`, {
89
+ headers: { Authorization: `Bearer ${apiKey}` },
90
+ });
91
+ if (!res.ok)
92
+ return null;
93
+ const data = (await res.json());
94
+ return data.id ?? null;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ function parseUrl(req, port) {
101
+ return new URL(req.url ?? "/", `http://localhost:${port}`);
102
+ }
103
+ function json(res, status, body) {
104
+ res.writeHead(status, { "Content-Type": "application/json" });
105
+ res.end(JSON.stringify(body));
106
+ }
107
+ export function startKynverMcpSseServer(options) {
108
+ const port = parseInt(process.env.PORT ?? String(options.defaultPort), 10);
109
+ const baseUrl = process.env.KYNVER_API_URL ?? "https://kynver.com";
110
+ const serviceSecret = process.env.KYNVER_SERVICE_SECRET ?? "";
111
+ const host = resolveListenHost();
112
+ const sessions = {};
113
+ const httpServer = createServer(async (req, res) => {
114
+ const url = parseUrl(req, port);
115
+ const path = url.pathname;
116
+ if (req.method === "GET" && path === "/sse") {
117
+ if (hasForbiddenQueryAuth(url)) {
118
+ json(res, 400, {
119
+ error: "Internal auth via query params is not allowed. Use X-Kynver-User-Id and X-Kynver-Service-Secret headers.",
120
+ });
121
+ return;
122
+ }
123
+ if (url.searchParams.has("apiKey")) {
124
+ console.warn("[MCP SSE] WARNING: apiKey in query string may leak via logs. Prefer Authorization header when your client supports it.");
125
+ }
126
+ let userId = null;
127
+ let auth = null;
128
+ const internal = serviceSecret ? resolveInternalAuth(req, serviceSecret) : null;
129
+ if (internal) {
130
+ userId = internal.userId;
131
+ auth = internal.auth;
132
+ }
133
+ else {
134
+ const apiKey = bearerToken(req) || queryApiKey(url);
135
+ if (apiKey) {
136
+ userId = await resolveUserFromApiKey(baseUrl, apiKey);
137
+ if (userId)
138
+ auth = { kind: "apiKey", apiKey };
139
+ }
140
+ }
141
+ if (!userId || !auth) {
142
+ json(res, 401, {
143
+ error: "Unauthorized. Pass API key via Authorization: Bearer <key> or internal X-Kynver-User-Id + X-Kynver-Service-Secret headers.",
144
+ });
145
+ return;
146
+ }
147
+ console.log(`[MCP SSE] New connection (${options.label}, user: ${userId.slice(0, 8)}...)`);
148
+ const transport = new SSEServerTransport("/messages", res);
149
+ sessions[transport.sessionId] = { transport, userId, auth };
150
+ res.on("close", () => {
151
+ delete sessions[transport.sessionId];
152
+ });
153
+ const server = await options.createMcpServer();
154
+ await server.connect(transport);
155
+ return;
156
+ }
157
+ if (req.method === "POST" && path === "/messages") {
158
+ const sessionId = url.searchParams.get("sessionId");
159
+ const entry = sessionId ? sessions[sessionId] : undefined;
160
+ if (!entry) {
161
+ json(res, 400, { error: "No active SSE session for this sessionId" });
162
+ return;
163
+ }
164
+ if (!verifyPostAuth(req, url, entry.auth, serviceSecret)) {
165
+ json(res, 401, { error: "Unauthorized. Re-send the same credentials used to open the SSE session." });
166
+ return;
167
+ }
168
+ try {
169
+ const body = await parseJsonBody(req);
170
+ await options.requestContext.run({ userId: entry.userId }, async () => {
171
+ await entry.transport.handlePostMessage(req, res, body);
172
+ });
173
+ }
174
+ catch (error) {
175
+ json(res, 400, {
176
+ error: error instanceof Error ? error.message : "Invalid request body",
177
+ });
178
+ }
179
+ return;
180
+ }
181
+ if (req.method === "GET" && path === "/health") {
182
+ res.writeHead(200, { "Content-Type": "text/plain" });
183
+ res.end("ok");
184
+ return;
185
+ }
186
+ json(res, 404, { error: "Not found" });
187
+ });
188
+ httpServer.listen(port, host, () => {
189
+ console.log(`[MCP SSE] ${options.label} listening on ${host}:${port}`);
190
+ });
191
+ }
package/dist/server.js CHANGED
@@ -69,6 +69,10 @@ export function createAgentOsServer() {
69
69
  return MCP_LEGACY_SLUG_FALLBACK;
70
70
  }
71
71
  const slugField = z.string().optional().describe("AgentOS slug within your account. Omit to use env or server-discovered primary (GET /api/agent-os). Legacy: `ghost`.");
72
+ const memorySurfaceField = z
73
+ .enum(["operator", "customer"])
74
+ .optional()
75
+ .describe("Response shape for memory reads. Owners/env admins may pass operator for full diagnostics or customer to preview grantee-safe JSON. Grantees always receive customer-safe fields regardless of this flag.");
72
76
  const sourceRefSchema = z
73
77
  .object({
74
78
  type: z
@@ -130,6 +134,7 @@ export function createAgentOsServer() {
130
134
  memoryQuery: z.string().optional().describe("Override the memory search query. Defaults to a query derived from the anchor's title/summary."),
131
135
  memoryLimit: z.number().optional().describe("Max memory hits (default 5, max 20). Set 0 to skip memory recall."),
132
136
  sessionLimit: z.number().optional().describe("Max recent sessions (default 1, max 5). Set 0 to skip."),
137
+ surface: memorySurfaceField,
133
138
  slug: slugField,
134
139
  }, async (args) => {
135
140
  const s = await resolveToolSlug(args.slug);
@@ -142,6 +147,8 @@ export function createAgentOsServer() {
142
147
  params.set("memoryLimit", String(args.memoryLimit));
143
148
  if (args.sessionLimit !== undefined)
144
149
  params.set("sessionLimit", String(args.sessionLimit));
150
+ if (args.surface)
151
+ params.set("surface", args.surface);
145
152
  return jsonResult(await get(`/agent-os/${s}/context-envelope?${params}`));
146
153
  });
147
154
  register("agent_os_open_session", "Call this at the START of every session. Records channel, model, and opens a session record for continuity. Ghost calls this immediately after agent_os_get_context on startup.", {
@@ -269,7 +276,7 @@ export function createAgentOsServer() {
269
276
  const s = await resolveToolSlug(slug);
270
277
  return jsonResult(await patch(`/agent-os/${s}/projects/${projectId}`, body));
271
278
  });
272
- register("agent_os_search_memory", "Semantic + keyword hybrid search over the agent's long-term MARM memory (ownerType=agent). Use to recall specific decisions, project details, or lessons without loading full context. Replaces grep/reading MEMORY.md. Optionally restrict to one source namespace via sourceId, or several via sourceIds (sourceId takes precedence when both are set). Results include rrfScore — a Reciprocal Rank Fusion score where ~0.033 is a near-perfect match. Higher is better; do not treat low numbers as poor results.", {
279
+ register("agent_os_search_memory", "Semantic + keyword hybrid search over the agent's long-term memory (ownerType=agent). Use to recall specific decisions, project details, or lessons without loading full context. Replaces grep/reading MEMORY.md. Optionally restrict to one source namespace via sourceId, or several via sourceIds (sourceId takes precedence when both are set). Workspace owners and env admins receive operator diagnostics (rrfScore, lanes, hygiene) unless surface=customer; grantees always receive grantee-safe hits (content, trust, evidence links only).", {
273
280
  query: z.string().describe("Natural language query to search memory."),
274
281
  k: z.number().optional().describe("Number of results (default 10, max 50)."),
275
282
  sourceId: z
@@ -282,6 +289,8 @@ export function createAgentOsServer() {
282
289
  .describe("Restrict search to any of these source namespaces. Ignored when sourceId is also set."),
283
290
  ticker: z.string().optional().describe("Trading desk: filter hits to this ticker symbol."),
284
291
  debatePersona: z.enum(["bull", "bear"]).optional().describe("Trading desk: filter to bull or bear advocate memories."),
292
+ personaSlug: z.string().optional().describe("Layer in this persona's operating rules in Lane A alongside global rules."),
293
+ surface: memorySurfaceField,
285
294
  slug: slugField,
286
295
  }, async (args) => {
287
296
  const params = new URLSearchParams({ q: args.query });
@@ -297,6 +306,10 @@ export function createAgentOsServer() {
297
306
  params.set("ticker", args.ticker);
298
307
  if (args.debatePersona)
299
308
  params.set("debatePersona", args.debatePersona);
309
+ if (args.personaSlug)
310
+ params.set("personaSlug", args.personaSlug);
311
+ if (args.surface)
312
+ params.set("surface", args.surface);
300
313
  const s = await resolveToolSlug(args.slug);
301
314
  return jsonResult(await get(`/agent-os/${s}/memory?${params}`));
302
315
  });
@@ -408,6 +421,14 @@ export function createAgentOsServer() {
408
421
  goalId: z.string().optional().describe("AgentOS goal id this memory supports, when known."),
409
422
  contactId: z.string().optional().describe("AgentOS contact id this memory concerns, when known."),
410
423
  skillId: z.string().optional().describe("Skill identifier this memory belongs to. Built-ins use skill slug; user-authored skills use AgentSkill.id."),
424
+ correctionReason: z
425
+ .string()
426
+ .optional()
427
+ .describe("When this write encodes a user/operator correction (e.g. Telegram preference fix), explain why — triggers memory-quality negative feedback."),
428
+ correctionSource: z
429
+ .enum(["telegram", "manual", "api", "session"])
430
+ .optional()
431
+ .describe("Channel that supplied the correction; stored in metadata for audit, not memory body."),
411
432
  slug: slugField,
412
433
  }, async (args) => {
413
434
  const categoryToSourceId = {
@@ -440,6 +461,10 @@ export function createAgentOsServer() {
440
461
  body.contactId = args.contactId;
441
462
  if (args.skillId)
442
463
  body.skillId = args.skillId;
464
+ if (args.correctionReason)
465
+ body.correctionReason = args.correctionReason;
466
+ if (args.correctionSource)
467
+ body.correctionSource = args.correctionSource;
443
468
  const s = await resolveToolSlug(args.slug);
444
469
  return jsonResult(await post(`/agent-os/${s}/memory`, body));
445
470
  });
@@ -536,9 +561,17 @@ export function createAgentOsServer() {
536
561
  goalId: z.string().optional(),
537
562
  contactId: z.string().optional(),
538
563
  skillId: z.string().optional().describe("Skill identifier this memory belongs to. Built-ins use skill slug; user-authored skills use AgentSkill.id."),
564
+ correctionReason: z
565
+ .string()
566
+ .optional()
567
+ .describe("Why this update corrects prior memory — triggers memory-quality negative feedback when set."),
568
+ correctionSource: z
569
+ .enum(["telegram", "manual", "api", "session"])
570
+ .optional()
571
+ .describe("Channel that supplied the correction."),
539
572
  slug: slugField,
540
573
  }, async (args) => {
541
- const { key, slug, content, sourceId, metadata, sourceRefs, memoryType, confidence, reviewStatus, projectId, goalId, contactId, skillId } = args;
574
+ const { key, slug, content, sourceId, metadata, sourceRefs, memoryType, confidence, reviewStatus, projectId, goalId, contactId, skillId, correctionReason, correctionSource } = args;
542
575
  const body = { content, slug: key };
543
576
  if (sourceId)
544
577
  body.sourceId = sourceId;
@@ -560,6 +593,10 @@ export function createAgentOsServer() {
560
593
  body.contactId = contactId;
561
594
  if (skillId)
562
595
  body.skillId = skillId;
596
+ if (correctionReason)
597
+ body.correctionReason = correctionReason;
598
+ if (correctionSource)
599
+ body.correctionSource = correctionSource;
563
600
  const s = await resolveToolSlug(slug);
564
601
  return jsonResult(await post(`/agent-os/${s}/memory`, body));
565
602
  });
@@ -579,6 +616,7 @@ export function createAgentOsServer() {
579
616
  goalId: z.string().optional(),
580
617
  contactId: z.string().optional(),
581
618
  skillId: z.string().optional(),
619
+ correctionSource: z.enum(["telegram", "manual", "api", "session"]).optional(),
582
620
  slug: slugField,
583
621
  }, async (args) => {
584
622
  const { slug, ...body } = args;
@@ -607,9 +645,10 @@ export function createAgentOsServer() {
607
645
  entityType: activeStateEntityTypeSchema.optional(),
608
646
  projectId: z.string().optional(),
609
647
  limit: z.number().optional(),
648
+ surface: memorySurfaceField,
610
649
  slug: slugField,
611
650
  }, async (args) => {
612
- const { slug, entityKey, entityType, projectId, limit } = args;
651
+ const { slug, entityKey, entityType, projectId, limit, surface } = args;
613
652
  const s = await resolveToolSlug(slug);
614
653
  const qs = new URLSearchParams();
615
654
  if (entityKey)
@@ -620,6 +659,8 @@ export function createAgentOsServer() {
620
659
  qs.set("projectId", projectId);
621
660
  if (limit !== undefined)
622
661
  qs.set("limit", String(limit));
662
+ if (surface)
663
+ qs.set("surface", surface);
623
664
  const suffix = qs.toString() ? "?" + qs.toString() : "";
624
665
  return jsonResult(await get("/agent-os/" + s + "/memory/state" + suffix));
625
666
  });
package/dist/sse.js CHANGED
@@ -1,4 +1,4 @@
1
- import { startKynverMcpSseServer } from "@kynver-app/mcp-sse-lib";
1
+ import { startKynverMcpSseServer } from "./lib/mcp-sse-server.js";
2
2
  import { createAgentOsServer } from "./server.js";
3
3
  import { requestContext } from "./lib/kynver-client.js";
4
4
  startKynverMcpSseServer({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kynver-app/mcp-agent-os",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Kynver Agentic OS MCP server — slug-keyed agent identity, goals, projects, sessions, and long-term memory",
5
5
  "type": "module",
6
6
  "bin": {