@kynver-app/mcp-agent-os 0.1.0
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 +76 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +11 -0
- package/dist/lib/kynver-client.d.ts +21 -0
- package/dist/lib/kynver-client.js +146 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +185 -0
- package/dist/sse.d.ts +13 -0
- package/dist/sse.js +130 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @kynver-app/mcp-agent-os
|
|
2
|
+
|
|
3
|
+
Kynver Agentic OS MCP server. Exposes the 12-tool `agent_os_*` family that
|
|
4
|
+
proxies to `/api/agent-os/{slug}/*` on a Kynver deployment. Each tool accepts
|
|
5
|
+
an optional `slug` argument; if omitted, it falls back to
|
|
6
|
+
`KYNVER_AGENT_OS_SLUG` env, then to `"ghost"`.
|
|
7
|
+
|
|
8
|
+
This is the **external surface only**. The in-app Kynver agent does not
|
|
9
|
+
dispatch these tools — they exist for MCP clients (Claude Code, Cursor,
|
|
10
|
+
OpenClaw, etc.) that connect to a running Kynver deployment.
|
|
11
|
+
|
|
12
|
+
## Tools
|
|
13
|
+
|
|
14
|
+
The full family:
|
|
15
|
+
|
|
16
|
+
- `agent_os_get_context`
|
|
17
|
+
- `agent_os_open_session`
|
|
18
|
+
- `agent_os_close_session`
|
|
19
|
+
- `agent_os_log_session`
|
|
20
|
+
- `agent_os_list_goals`
|
|
21
|
+
- `agent_os_update_goal`
|
|
22
|
+
- `agent_os_get_projects`
|
|
23
|
+
- `agent_os_update_project`
|
|
24
|
+
- `agent_os_search_memory`
|
|
25
|
+
- `agent_os_write_memory`
|
|
26
|
+
- `agent_os_get_contacts`
|
|
27
|
+
- `agent_os_consolidate_memory`
|
|
28
|
+
|
|
29
|
+
Schemas are mirrored in `lib/mcp/tool-manifests/kynver-mcp-agent-os.json` in
|
|
30
|
+
the Kynver repo (used as the static fallback when stdio spawn isn't available
|
|
31
|
+
— e.g. on Vercel).
|
|
32
|
+
|
|
33
|
+
## Env
|
|
34
|
+
|
|
35
|
+
| Var | Purpose |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `KYNVER_API_URL` | Kynver origin, e.g. `https://kynver.com` |
|
|
38
|
+
| `KYNVER_API_KEY` | Bearer token for external authentication |
|
|
39
|
+
| `KYNVER_AGENT_OS_SLUG` | Default agent slug when the tool's `slug` arg is omitted (defaults to `"ghost"`) |
|
|
40
|
+
| `KYNVER_SERVICE_SECRET` | Required when running under SSE and forwarding internal `userId` headers |
|
|
41
|
+
| `PORT` | SSE port (defaults to `3101`) |
|
|
42
|
+
|
|
43
|
+
## OpenClaw / Claude Code config
|
|
44
|
+
|
|
45
|
+
Once this package is published, the Ghost workspace's MCP config needs **two
|
|
46
|
+
separate** Kynver MCP server entries — one for the analyst surface and one for
|
|
47
|
+
this Agentic-OS surface:
|
|
48
|
+
|
|
49
|
+
```jsonc
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"kynver-analyst": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "@kynver-app/mcp-analyst"],
|
|
55
|
+
"env": {
|
|
56
|
+
"KYNVER_API_URL": "https://kynver.com",
|
|
57
|
+
"KYNVER_API_KEY": "${KYNVER_API_KEY}"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"kynver-agent-os": {
|
|
61
|
+
"command": "npx",
|
|
62
|
+
"args": ["-y", "@kynver-app/mcp-agent-os"],
|
|
63
|
+
"env": {
|
|
64
|
+
"KYNVER_API_URL": "https://kynver.com",
|
|
65
|
+
"KYNVER_API_KEY": "${KYNVER_API_KEY}",
|
|
66
|
+
"KYNVER_AGENT_OS_SLUG": "ghost"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The split mirrors the server-side change of 2026-05-14: the `agent_os_*`
|
|
74
|
+
family was extracted from `@kynver-app/mcp-analyst` into this dedicated
|
|
75
|
+
package so each AgentOS deployment can mount it independently from the
|
|
76
|
+
analyst tools.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Kynver Agentic OS MCP server — stdio entry point.
|
|
4
|
+
* Run: node dist/index.js
|
|
5
|
+
* Env: KYNVER_API_URL, KYNVER_API_KEY (required)
|
|
6
|
+
*/
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { createAgentOsServer } from "./server.js";
|
|
9
|
+
const server = createAgentOsServer();
|
|
10
|
+
const transport = new StdioServerTransport();
|
|
11
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin fetch wrapper for Kynver API. Base URL from KYNVER_API_URL; optional Bearer from KYNVER_API_KEY.
|
|
3
|
+
*
|
|
4
|
+
* Supports per-request user identity via AsyncLocalStorage. When a userId is
|
|
5
|
+
* set in the request context (e.g. from the SSE route), all API calls include
|
|
6
|
+
* X-Kynver-User-Id and X-Kynver-Service-Secret headers so the Kynver API
|
|
7
|
+
* executes operations on behalf of the correct user.
|
|
8
|
+
*/
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
interface RequestContext {
|
|
11
|
+
userId?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const requestContext: AsyncLocalStorage<RequestContext>;
|
|
14
|
+
export declare function get<T = unknown>(path: string): Promise<T>;
|
|
15
|
+
export declare function post<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
16
|
+
export declare function patch<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
17
|
+
export declare function put<T = unknown>(path: string, body: unknown): Promise<T>;
|
|
18
|
+
export declare function del<T = unknown>(path: string): Promise<T>;
|
|
19
|
+
export declare function hasApiKey(): boolean;
|
|
20
|
+
export declare function noApiKeyMessage(): string;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin fetch wrapper for Kynver API. Base URL from KYNVER_API_URL; optional Bearer from KYNVER_API_KEY.
|
|
3
|
+
*
|
|
4
|
+
* Supports per-request user identity via AsyncLocalStorage. When a userId is
|
|
5
|
+
* set in the request context (e.g. from the SSE route), all API calls include
|
|
6
|
+
* X-Kynver-User-Id and X-Kynver-Service-Secret headers so the Kynver API
|
|
7
|
+
* executes operations on behalf of the correct user.
|
|
8
|
+
*/
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
export const requestContext = new AsyncLocalStorage();
|
|
11
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 120_000;
|
|
12
|
+
const MAX_ERROR_MESSAGE_LEN = 2_000;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Config (read per call so late-loaded env in tests / dotenv works)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function getBaseUrl() {
|
|
17
|
+
const u = process.env.KYNVER_API_URL?.trim();
|
|
18
|
+
if (u)
|
|
19
|
+
return u.replace(/\/$/, "");
|
|
20
|
+
if (process.env.NODE_ENV === "development") {
|
|
21
|
+
return "http://localhost:3000";
|
|
22
|
+
}
|
|
23
|
+
throw new Error("KYNVER_API_URL is not set. Set it to your Kynver app origin (e.g. https://kynver.com or http://localhost:3000).");
|
|
24
|
+
}
|
|
25
|
+
function getApiKey() {
|
|
26
|
+
return process.env.KYNVER_API_KEY?.trim() ?? "";
|
|
27
|
+
}
|
|
28
|
+
function assertSafeApiPath(path) {
|
|
29
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
30
|
+
if (p.includes("..") || p.includes("\0")) {
|
|
31
|
+
throw new Error("Invalid API path: path traversal is not allowed");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function url(path) {
|
|
35
|
+
assertSafeApiPath(path);
|
|
36
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
37
|
+
const apiPath = p.startsWith("/api") ? p : `/api${p}`;
|
|
38
|
+
return `${getBaseUrl()}${apiPath}`;
|
|
39
|
+
}
|
|
40
|
+
function getHeaders() {
|
|
41
|
+
const headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
};
|
|
44
|
+
const apiKey = getApiKey();
|
|
45
|
+
if (apiKey) {
|
|
46
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
47
|
+
}
|
|
48
|
+
const ctx = requestContext.getStore();
|
|
49
|
+
const userId = ctx?.userId;
|
|
50
|
+
const serviceSecret = process.env.KYNVER_SERVICE_SECRET;
|
|
51
|
+
if (userId) {
|
|
52
|
+
if (!serviceSecret) {
|
|
53
|
+
throw new Error("X-Kynver-User-Id was set in context but KYNVER_SERVICE_SECRET is missing; refusing to send an unscoped request.");
|
|
54
|
+
}
|
|
55
|
+
headers["X-Kynver-User-Id"] = userId;
|
|
56
|
+
headers["X-Kynver-Service-Secret"] = serviceSecret;
|
|
57
|
+
}
|
|
58
|
+
return headers;
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Response handling
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
function normalizeErrorMessage(msg) {
|
|
64
|
+
const s = String(msg).replace(/\s+/g, " ").trim();
|
|
65
|
+
return s.length > MAX_ERROR_MESSAGE_LEN ? `${s.slice(0, MAX_ERROR_MESSAGE_LEN)}…` : s;
|
|
66
|
+
}
|
|
67
|
+
async function handleResponse(res) {
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
let msg = text;
|
|
71
|
+
try {
|
|
72
|
+
const j = JSON.parse(text);
|
|
73
|
+
msg = j.error ?? j.message ?? text;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// use text as-is
|
|
77
|
+
}
|
|
78
|
+
throw new Error(normalizeErrorMessage(msg || `HTTP ${res.status}`));
|
|
79
|
+
}
|
|
80
|
+
if (!text)
|
|
81
|
+
return undefined;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function fetchWithTimeout(path, init) {
|
|
90
|
+
const timeoutMs = Number(process.env.KYNVER_FETCH_TIMEOUT_MS ?? DEFAULT_FETCH_TIMEOUT_MS);
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
return await fetch(url(path), { ...init, signal: controller.signal });
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
clearTimeout(t);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// HTTP methods
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
export async function get(path) {
|
|
104
|
+
const res = await fetchWithTimeout(path, {
|
|
105
|
+
method: "GET",
|
|
106
|
+
headers: getHeaders(),
|
|
107
|
+
});
|
|
108
|
+
return handleResponse(res);
|
|
109
|
+
}
|
|
110
|
+
export async function post(path, body) {
|
|
111
|
+
const res = await fetchWithTimeout(path, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: getHeaders(),
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
});
|
|
116
|
+
return handleResponse(res);
|
|
117
|
+
}
|
|
118
|
+
export async function patch(path, body) {
|
|
119
|
+
const res = await fetchWithTimeout(path, {
|
|
120
|
+
method: "PATCH",
|
|
121
|
+
headers: getHeaders(),
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
});
|
|
124
|
+
return handleResponse(res);
|
|
125
|
+
}
|
|
126
|
+
export async function put(path, body) {
|
|
127
|
+
const res = await fetchWithTimeout(path, {
|
|
128
|
+
method: "PUT",
|
|
129
|
+
headers: getHeaders(),
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
});
|
|
132
|
+
return handleResponse(res);
|
|
133
|
+
}
|
|
134
|
+
export async function del(path) {
|
|
135
|
+
const res = await fetchWithTimeout(path, {
|
|
136
|
+
method: "DELETE",
|
|
137
|
+
headers: getHeaders(),
|
|
138
|
+
});
|
|
139
|
+
return handleResponse(res);
|
|
140
|
+
}
|
|
141
|
+
export function hasApiKey() {
|
|
142
|
+
return !!getApiKey();
|
|
143
|
+
}
|
|
144
|
+
export function noApiKeyMessage() {
|
|
145
|
+
return "No API key configured. Set KYNVER_API_KEY in your MCP server config.";
|
|
146
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kynver Agentic OS MCP server: slug-keyed agent identity, goals, projects,
|
|
3
|
+
* sessions, contacts, and long-term memory.
|
|
4
|
+
*
|
|
5
|
+
* Each tool proxies to `/api/agent-os/{slug}/*` on Kynver — those routes are
|
|
6
|
+
* admin-only. The `slug` argument is optional on every tool; if omitted, it
|
|
7
|
+
* falls back to `KYNVER_AGENT_OS_SLUG` env, then to `"ghost"`. This is the
|
|
8
|
+
* twelve-tool `agent_os_*` family — previously colocated with `kynver-mcp-analyst`,
|
|
9
|
+
* split into its own package so each AgentOS deployment can mount this server
|
|
10
|
+
* independently from the analyst surface.
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
export declare function createAgentOsServer(): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kynver Agentic OS MCP server: slug-keyed agent identity, goals, projects,
|
|
3
|
+
* sessions, contacts, and long-term memory.
|
|
4
|
+
*
|
|
5
|
+
* Each tool proxies to `/api/agent-os/{slug}/*` on Kynver — those routes are
|
|
6
|
+
* admin-only. The `slug` argument is optional on every tool; if omitted, it
|
|
7
|
+
* falls back to `KYNVER_AGENT_OS_SLUG` env, then to `"ghost"`. This is the
|
|
8
|
+
* twelve-tool `agent_os_*` family — previously colocated with `kynver-mcp-analyst`,
|
|
9
|
+
* split into its own package so each AgentOS deployment can mount this server
|
|
10
|
+
* independently from the analyst surface.
|
|
11
|
+
*/
|
|
12
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { get, post, patch } from "./lib/kynver-client.js";
|
|
15
|
+
function toolResult(text) {
|
|
16
|
+
return { content: [{ type: "text", text }] };
|
|
17
|
+
}
|
|
18
|
+
function jsonResult(data) {
|
|
19
|
+
return toolResult(JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
export function createAgentOsServer() {
|
|
22
|
+
const server = new McpServer({ name: "kynver-mcp-agent-os", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
23
|
+
// The deprecated `server.tool(name, desc, zodShape, handler)` overloads
|
|
24
|
+
// generate a heavy generic type graph that OOMs tsc when many tools share
|
|
25
|
+
// one file. We use `registerTool` with an `as any` cast — same pattern the
|
|
26
|
+
// estimator/contents packages already use — so the build stays fast.
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
function register(name, description, inputSchema, cb) {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
server.registerTool(name, { description, inputSchema }, cb);
|
|
31
|
+
}
|
|
32
|
+
// ─── Agentic OS Tools ──────────────────────────────────────────────
|
|
33
|
+
// Proxy to /api/agent-os/{slug}/* (admin-only on Kynver). Each tool takes
|
|
34
|
+
// an optional `slug` (the AgentOS slug — Ghost, Aria, etc.); if omitted it
|
|
35
|
+
// falls back to KYNVER_AGENT_OS_SLUG, then "ghost". The Ghost-scoped
|
|
36
|
+
// `agent_os_*` aliases and the legacy `agentOS_*` set were unified into
|
|
37
|
+
// this single snake_case family.
|
|
38
|
+
const defaultAgentOsSlug = () => process.env.KYNVER_AGENT_OS_SLUG?.trim() || "ghost";
|
|
39
|
+
const osSlug = (s) => (s && s.trim()) || defaultAgentOsSlug();
|
|
40
|
+
register("agent_os_get_context", "Get the agent's full Agentic-OS state in one call: identity, open goals, active projects with current focus/next-actions/blockers, recent sessions, contacts, and long-term memory stats. Use at session start instead of reading SOUL.md / USER.md / MEMORY.md.", { slug: z.string().optional().describe("AgentOS slug. Defaults to KYNVER_AGENT_OS_SLUG, then 'ghost'.") }, async (args) => jsonResult(await get(`/agent-os/${osSlug(args.slug)}/stats`)));
|
|
41
|
+
register("agent_os_open_session", "Mark the start of a session. Returns the session id — pass it to agent_os_close_session at the end.", {
|
|
42
|
+
channel: z.string().describe("Runtime channel: 'webchat' | 'telegram' | 'discord' | …"),
|
|
43
|
+
model: z.string().optional().describe("Model used for the session (default 'claude-sonnet-4-6')."),
|
|
44
|
+
slug: z.string().optional().describe("AgentOS slug. Defaults to KYNVER_AGENT_OS_SLUG, then 'ghost'."),
|
|
45
|
+
}, async (args) => {
|
|
46
|
+
const { slug, ...body } = args;
|
|
47
|
+
return jsonResult(await post(`/agent-os/${osSlug(slug)}/sessions`, body));
|
|
48
|
+
});
|
|
49
|
+
register("agent_os_close_session", "Close a session with an end-of-session summary (2-3 sentences), the topics worked on, and optional decisions log / touched goal & project ids. The summary is ingested into long-term memory.", {
|
|
50
|
+
sessionId: z.string(),
|
|
51
|
+
summary: z.string(),
|
|
52
|
+
topicsWorked: z.array(z.string()).optional(),
|
|
53
|
+
decisionsLog: z.unknown().optional(),
|
|
54
|
+
goalIds: z.array(z.string()).optional(),
|
|
55
|
+
projectIds: z.array(z.string()).optional(),
|
|
56
|
+
slug: z.string().optional(),
|
|
57
|
+
}, async (args) => {
|
|
58
|
+
const { slug, sessionId, ...body } = args;
|
|
59
|
+
return jsonResult(await patch(`/agent-os/${osSlug(slug)}/sessions/${sessionId}`, body));
|
|
60
|
+
});
|
|
61
|
+
register("agent_os_log_session", "Append a structured session-log entry to the agent's daily log. Call at session end or mid-session checkpoints. Replaces writing to memory/YYYY-MM-DD.md. Entries accumulate across sessions; consolidation distils them into long-term memory.", {
|
|
62
|
+
summary: z.string().describe("2-4 sentence summary of what was worked on."),
|
|
63
|
+
topicsWorked: z.array(z.string()).optional().describe("Topics covered this session."),
|
|
64
|
+
keyDecisions: z.array(z.string()).optional().describe("Important decisions made."),
|
|
65
|
+
date: z.string().optional().describe("Date to log against (YYYY-MM-DD, defaults to today UTC)."),
|
|
66
|
+
slug: z.string().optional().describe("AgentOS slug. Defaults to KYNVER_AGENT_OS_SLUG, then 'ghost'."),
|
|
67
|
+
}, async (args) => {
|
|
68
|
+
const lines = [args.summary];
|
|
69
|
+
if (args.topicsWorked?.length)
|
|
70
|
+
lines.push(`Topics: ${args.topicsWorked.join(", ")}`);
|
|
71
|
+
if (args.keyDecisions?.length) {
|
|
72
|
+
lines.push("Key decisions:");
|
|
73
|
+
args.keyDecisions.forEach((d) => lines.push(`- ${d}`));
|
|
74
|
+
}
|
|
75
|
+
const entry = lines.join("\n");
|
|
76
|
+
return jsonResult(await post(`/agent-os/${osSlug(args.slug)}/daily-log`, {
|
|
77
|
+
entry,
|
|
78
|
+
...(args.date ? { date: args.date } : {}),
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
register("agent_os_list_goals", "List the agent's goals, optionally filtered by status or project. Replaces reading the pending sections of MEMORY.md.", {
|
|
82
|
+
status: z
|
|
83
|
+
.enum(["open", "in_progress", "blocked", "complete", "cancelled"])
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Filter by status. Omit to get all goals."),
|
|
86
|
+
projectId: z.string().optional().describe("Filter by project DB ID."),
|
|
87
|
+
slug: z.string().optional(),
|
|
88
|
+
}, async (args) => {
|
|
89
|
+
const params = new URLSearchParams();
|
|
90
|
+
if (args.status)
|
|
91
|
+
params.set("status", args.status);
|
|
92
|
+
if (args.projectId)
|
|
93
|
+
params.set("projectId", args.projectId);
|
|
94
|
+
const qs = params.toString();
|
|
95
|
+
return jsonResult(await get(`/agent-os/${osSlug(args.slug)}/goals${qs ? `?${qs}` : ""}`));
|
|
96
|
+
});
|
|
97
|
+
register("agent_os_update_goal", "Create a new goal or update an existing one. Pass goalId to update; omit to create (title is then required). Set status=complete with an outcome to close a goal (stamps closedAt). projectId is the project DB ID from agent_os_get_projects.", {
|
|
98
|
+
title: z.string().optional().describe("Goal title. Required when creating."),
|
|
99
|
+
description: z.string().optional(),
|
|
100
|
+
status: z.enum(["open", "in_progress", "blocked", "complete", "cancelled"]).optional(),
|
|
101
|
+
priority: z.enum(["low", "normal", "high", "critical"]).optional(),
|
|
102
|
+
projectId: z.string().optional().describe("Project DB ID to associate."),
|
|
103
|
+
analystHypothesisId: z.string().optional().describe("Optional analyst hypothesis to link (create-only)."),
|
|
104
|
+
outcome: z.string().optional().describe("Outcome — include when closing a goal."),
|
|
105
|
+
goalId: z.string().optional().describe("ID of existing goal to update. Omit to create."),
|
|
106
|
+
slug: z.string().optional(),
|
|
107
|
+
}, async (args) => {
|
|
108
|
+
const { goalId, slug, ...body } = args;
|
|
109
|
+
if (goalId) {
|
|
110
|
+
return jsonResult(await patch(`/agent-os/${osSlug(slug)}/goals/${goalId}`, body));
|
|
111
|
+
}
|
|
112
|
+
if (!body.title) {
|
|
113
|
+
return jsonResult({ error: "title is required when creating a goal" });
|
|
114
|
+
}
|
|
115
|
+
return jsonResult(await post(`/agent-os/${osSlug(slug)}/goals`, body));
|
|
116
|
+
});
|
|
117
|
+
register("agent_os_get_projects", "Get all projects the agent is tracking with current focus, next actions, blockers, and status. Replaces reading project sections of MEMORY.md.", {
|
|
118
|
+
status: z
|
|
119
|
+
.enum(["active", "on_hold", "shipped", "archived"])
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("Filter by project status. Omit to get all."),
|
|
122
|
+
slug: z.string().optional(),
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
const params = new URLSearchParams();
|
|
125
|
+
if (args.status)
|
|
126
|
+
params.set("status", args.status);
|
|
127
|
+
const qs = params.toString();
|
|
128
|
+
return jsonResult(await get(`/agent-os/${osSlug(args.slug)}/projects${qs ? `?${qs}` : ""}`));
|
|
129
|
+
});
|
|
130
|
+
register("agent_os_update_project", "Update a project's current focus, next-actions queue, blockers list, status, or repo/deploy URLs. The projectId is the DB ID from agent_os_get_projects.", {
|
|
131
|
+
projectId: z.string().describe("Project DB ID (from agent_os_get_projects)."),
|
|
132
|
+
name: z.string().optional(),
|
|
133
|
+
description: z.string().optional(),
|
|
134
|
+
status: z.enum(["active", "on_hold", "shipped", "archived"]).optional(),
|
|
135
|
+
currentFocus: z.string().optional().describe("What's being worked on right now."),
|
|
136
|
+
nextActions: z.array(z.string()).optional().describe("Ordered list of next steps."),
|
|
137
|
+
blockers: z.array(z.string()).optional().describe("Current blockers."),
|
|
138
|
+
repoUrl: z.string().optional(),
|
|
139
|
+
deployUrl: z.string().optional(),
|
|
140
|
+
slug: z.string().optional(),
|
|
141
|
+
}, async (args) => {
|
|
142
|
+
const { projectId, slug, ...body } = args;
|
|
143
|
+
return jsonResult(await patch(`/agent-os/${osSlug(slug)}/projects/${projectId}`, body));
|
|
144
|
+
});
|
|
145
|
+
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.", {
|
|
146
|
+
query: z.string().describe("Natural language query to search memory."),
|
|
147
|
+
k: z.number().optional().describe("Number of results (default 10, max 50)."),
|
|
148
|
+
slug: z.string().optional(),
|
|
149
|
+
}, async (args) => {
|
|
150
|
+
const params = new URLSearchParams({ q: args.query });
|
|
151
|
+
if (args.k)
|
|
152
|
+
params.set("k", String(args.k));
|
|
153
|
+
return jsonResult(await get(`/agent-os/${osSlug(args.slug)}/memory?${params}`));
|
|
154
|
+
});
|
|
155
|
+
register("agent_os_write_memory", "Write a durable memory entry to the agent's MARM. Use to persist decisions, lessons, or key facts across sessions. Replaces appending to MEMORY.md. Either pass a category enum (mapped to sourceId) or override sourceId directly.", {
|
|
156
|
+
content: z.string().describe("The memory content to store."),
|
|
157
|
+
key: z.string().optional().describe("Unique slug identifier for this memory (e.g. 'kynver-arch-decision-2026-05'). Defaults to a timestamped note slug."),
|
|
158
|
+
category: z
|
|
159
|
+
.enum(["long_term", "project", "contact", "tool_config"])
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("Memory category. Mapped to sourceId: long_term/contact/tool_config -> 'agent:long-term', project -> 'agent:project'. Ignored if sourceId is set."),
|
|
162
|
+
sourceId: z.string().optional().describe("Advanced: override the sourceId tag directly. If set, category is ignored."),
|
|
163
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
164
|
+
slug: z.string().optional().describe("AgentOS slug. Defaults to KYNVER_AGENT_OS_SLUG, then 'ghost'."),
|
|
165
|
+
}, async (args) => {
|
|
166
|
+
const categoryToSourceId = {
|
|
167
|
+
long_term: "agent:long-term",
|
|
168
|
+
project: "agent:project",
|
|
169
|
+
contact: "agent:long-term",
|
|
170
|
+
tool_config: "agent:long-term",
|
|
171
|
+
};
|
|
172
|
+
const sourceId = args.sourceId ?? (args.category ? categoryToSourceId[args.category] : undefined);
|
|
173
|
+
const body = { content: args.content };
|
|
174
|
+
if (args.key)
|
|
175
|
+
body.slug = args.key;
|
|
176
|
+
if (sourceId)
|
|
177
|
+
body.sourceId = sourceId;
|
|
178
|
+
if (args.metadata)
|
|
179
|
+
body.metadata = args.metadata;
|
|
180
|
+
return jsonResult(await post(`/agent-os/${osSlug(args.slug)}/memory`, body));
|
|
181
|
+
});
|
|
182
|
+
register("agent_os_get_contacts", "Get all people the agent knows: their relationship, context, preferences, and notes. Replaces reading USER.md and contact sections of MEMORY.md.", { slug: z.string().optional() }, async (args) => jsonResult(await get(`/agent-os/${osSlug(args.slug)}/contacts`)));
|
|
183
|
+
register("agent_os_consolidate_memory", "Trigger a memory consolidation pass: LLM reviews recent daily logs and distils key decisions, lessons, and facts into long-term MARM memory. Use during heartbeat maintenance instead of manually rewriting MEMORY.md. Rate-limited to 2 runs per agent per day.", { slug: z.string().optional() }, async (args) => jsonResult(await post(`/agent-os/${osSlug(args.slug)}/consolidate`, {})));
|
|
184
|
+
return server;
|
|
185
|
+
}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE entry point for the Agentic OS MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Supports two authentication modes:
|
|
5
|
+
* 1. Internal (Kynver dashboard): userId + KYNVER_SERVICE_SECRET query params
|
|
6
|
+
* 2. External (Claude Code, Cursor, any MCP client): Kynver API key via
|
|
7
|
+
* query param (?apiKey=…) or Authorization header
|
|
8
|
+
*
|
|
9
|
+
* External keys are validated by calling GET /api/users/me on the Kynver API.
|
|
10
|
+
* Once authenticated, tool calls execute on behalf of the resolved user via
|
|
11
|
+
* the requestContext AsyncLocalStorage.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE entry point for the Agentic OS MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Supports two authentication modes:
|
|
5
|
+
* 1. Internal (Kynver dashboard): userId + KYNVER_SERVICE_SECRET query params
|
|
6
|
+
* 2. External (Claude Code, Cursor, any MCP client): Kynver API key via
|
|
7
|
+
* query param (?apiKey=…) or Authorization header
|
|
8
|
+
*
|
|
9
|
+
* External keys are validated by calling GET /api/users/me on the Kynver API.
|
|
10
|
+
* Once authenticated, tool calls execute on behalf of the resolved user via
|
|
11
|
+
* the requestContext AsyncLocalStorage.
|
|
12
|
+
*/
|
|
13
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
14
|
+
import { createServer } from "node:http";
|
|
15
|
+
import { createAgentOsServer } from "./server.js";
|
|
16
|
+
import { requestContext } from "./lib/kynver-client.js";
|
|
17
|
+
const PORT = parseInt(process.env.PORT ?? "3101", 10);
|
|
18
|
+
const BASE_URL = process.env.KYNVER_API_URL ?? "https://kynver.com";
|
|
19
|
+
const SERVICE_SECRET = process.env.KYNVER_SERVICE_SECRET ?? "";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Session tracking — maps sessionId → { transport, userId }
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const sessions = {};
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** Validate an external API key by calling the Kynver API. */
|
|
28
|
+
async function resolveUser(apiKey) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`${BASE_URL}/api/users/me`, {
|
|
31
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
return null;
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
return data.id ?? null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Read and JSON-parse the request body (needed for raw http.Server). */
|
|
43
|
+
function parseBody(req) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
47
|
+
req.on("end", () => {
|
|
48
|
+
try {
|
|
49
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString()));
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
reject(e);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
req.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function parseUrl(req) {
|
|
59
|
+
return new URL(req.url ?? "/", `http://localhost:${PORT}`);
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// HTTP server
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
const httpServer = createServer(async (req, res) => {
|
|
65
|
+
const url = parseUrl(req);
|
|
66
|
+
const path = url.pathname;
|
|
67
|
+
// ── SSE connection ──────────────────────────────────────────────────
|
|
68
|
+
if (req.method === "GET" && path === "/sse") {
|
|
69
|
+
// Internal auth: Kynver dashboard passes userId + service secret
|
|
70
|
+
const internalUserId = url.searchParams.get("userId");
|
|
71
|
+
const internalSecret = url.searchParams.get("secret");
|
|
72
|
+
// External auth: API key via query param or Authorization header
|
|
73
|
+
const apiKey = url.searchParams.get("apiKey") ??
|
|
74
|
+
req.headers.authorization?.replace("Bearer ", "") ??
|
|
75
|
+
"";
|
|
76
|
+
let userId = null;
|
|
77
|
+
if (internalUserId && SERVICE_SECRET && internalSecret === SERVICE_SECRET) {
|
|
78
|
+
userId = internalUserId;
|
|
79
|
+
}
|
|
80
|
+
else if (apiKey) {
|
|
81
|
+
userId = await resolveUser(apiKey);
|
|
82
|
+
}
|
|
83
|
+
if (!userId) {
|
|
84
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
85
|
+
res.end(JSON.stringify({
|
|
86
|
+
error: "Unauthorized. Provide a valid Kynver API key.",
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(`[MCP SSE] New connection (userId: ${userId})`);
|
|
91
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
92
|
+
sessions[transport.sessionId] = { transport, userId };
|
|
93
|
+
res.on("close", () => {
|
|
94
|
+
console.log(`[MCP SSE] Connection closed: ${transport.sessionId}`);
|
|
95
|
+
delete sessions[transport.sessionId];
|
|
96
|
+
});
|
|
97
|
+
const server = createAgentOsServer();
|
|
98
|
+
await server.connect(transport);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// ── Message handler (client → server JSON-RPC messages) ─────────────
|
|
102
|
+
if (req.method === "POST" && path === "/messages") {
|
|
103
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
104
|
+
const entry = sessionId ? sessions[sessionId] : undefined;
|
|
105
|
+
if (!entry) {
|
|
106
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
107
|
+
res.end(JSON.stringify({
|
|
108
|
+
error: "No active SSE session for this sessionId",
|
|
109
|
+
}));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const body = await parseBody(req);
|
|
113
|
+
// Wrap in request context so kynver-client sends the right user headers
|
|
114
|
+
await requestContext.run({ userId: entry.userId }, async () => {
|
|
115
|
+
await entry.transport.handlePostMessage(req, res, body);
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// ── Health check ────────────────────────────────────────────────────
|
|
120
|
+
if (req.method === "GET" && path === "/health") {
|
|
121
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
122
|
+
res.end("ok");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
126
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
127
|
+
});
|
|
128
|
+
httpServer.listen(PORT, () => {
|
|
129
|
+
console.log(`[MCP SSE] Agentic OS server listening on port ${PORT}`);
|
|
130
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kynver-app/mcp-agent-os",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kynver Agentic OS MCP server — slug-keyed agent identity, goals, projects, sessions, and long-term memory",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kynver-mcp-agent-os": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist", "README.md"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "node --max-old-space-size=8192 ../../node_modules/typescript/bin/tsc",
|
|
12
|
+
"dev": "node --max-old-space-size=8192 ../../node_modules/typescript/bin/tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"start:sse": "node dist/sse.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
21
|
+
"zod": "^3.23.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22",
|
|
25
|
+
"typescript": "^5.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|