@calypso-rag/calypso-mcp 1.0.23

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Calypso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # Calypso RAG MCP Server
2
+
3
+ [![smithery badge](https://smithery.ai/badge/multimodal-rag/calypso-mcp-server)](https://smithery.ai/servers/multimodal-rag/calypso-mcp-server)
4
+
5
+ This MCP server exposes the **Calypso RAG agent** to MCP clients such as Cursor and Claude Desktop. It is a thin bridge to Calypso's OpenAI-compatible API and forwards every request to the `calypso-rag-agent` model.
6
+
7
+ Docs: `https://docs.calypso.ms/`
8
+
9
+ ## What you get
10
+
11
+ - **`calypso-rag-agent`**: a single tool that sends each turn directly to the Calypso RAG agent
12
+
13
+ The tool accepts a single `prompt` argument.
14
+
15
+ ## What this MCP does
16
+
17
+ With `calypso-rag-agent` you can:
18
+
19
+ - Ask grounded questions against the configured Calypso knowledge base
20
+ - Continue a multi-turn conversation via the native `/v1/responses` conversation model
21
+ - Reset the conversation context with `/new`
22
+ - Use the same OpenAI-compatible Responses endpoint that serves `calypso-rag-agent`
23
+
24
+ ## Requirements
25
+
26
+ - Node.js 18+
27
+ - A Calypso API endpoint that exposes:
28
+ - `POST /v1/responses`
29
+ - A Calypso API key (`sk-...`)
30
+
31
+ ## Configuration
32
+
33
+ Environment variables:
34
+
35
+ - `CALYPSO_API_KEY` (required)
36
+ - `CALYPSO_API_BASE_URL` (optional, default `https://api.calypso.so/v1`)
37
+
38
+ CLI flags:
39
+
40
+ - `--api-key`
41
+ - `--api-base-url`
42
+
43
+ Configuration precedence:
44
+
45
+ 1. CLI flags / Smithery-provided command arguments
46
+ 2. Environment variables
47
+ 3. Default base URL (`https://api.calypso.so/v1`)
48
+
49
+ ## Run with npx
50
+
51
+ ```bash
52
+ npx -y @calypso-rag/calypso-mcp --api-key "sk-..."
53
+ ```
54
+
55
+ ## Run with environment variables
56
+
57
+ ```bash
58
+ env CALYPSO_API_KEY="sk-..." CALYPSO_API_BASE_URL="https://api.calypso.so/v1" npx -y @calypso-rag/calypso-mcp
59
+ ```
60
+
61
+ ## Configure in Cursor
62
+
63
+ Add a new MCP server (command type) like:
64
+
65
+ ```bash
66
+ npx -y @calypso-rag/calypso-mcp --api-key sk-... --api-base-url https://api.calypso.so/v1
67
+ ```
68
+
69
+ ## Smithery
70
+
71
+ This repo includes a [`smithery.yaml`](./smithery.yaml) manifest that launches the published package with CLI flags instead of relying on a prebuilt local `dist/` directory.
72
+
73
+ For local `.mcpb` publishing, Smithery capabilities are populated from a full MCP-style server card. This repo keeps that metadata in [`smithery.server-card.json`](./smithery.server-card.json) and publishes it with [`scripts/publish-smithery.mjs`](./scripts/publish-smithery.mjs), because MCPB `manifest.json` does not support the full `inputSchema` shape Smithery expects for capabilities.
74
+
75
+ Smithery user config:
76
+
77
+ - `calypsoApiKey` (required)
78
+ - `calypsoApiBaseUrl` (optional, defaults to `https://api.calypso.so/v1`)
79
+
80
+ The Smithery launch path is equivalent to:
81
+
82
+ ```bash
83
+ npx -y @calypso-rag/calypso-mcp --api-key sk-... --api-base-url https://api.calypso.so/v1
84
+ ```
85
+
86
+ Use `calypsoApiBaseUrl` only when targeting a self-hosted Calypso-compatible deployment. The cloud default does not need an override.
87
+
88
+ ## Publish-readiness validation
89
+
90
+ Before publishing to Smithery, run:
91
+
92
+ ```bash
93
+ npm run validate:smithery
94
+ ```
95
+
96
+ That validation flow does two things:
97
+
98
+ - builds the package
99
+ - runs a local stdio smoke test that launches the built server and verifies that `calypso-rag-agent` is registered
100
+
101
+ You can also run the smoke test directly after a build:
102
+
103
+ ```bash
104
+ npm run build
105
+ npm run smoke:stdio
106
+ ```
107
+
108
+ To build the local Smithery / MCPB bundle:
109
+
110
+ ```bash
111
+ npm run build:mcpb
112
+ ```
113
+
114
+ That produces `server.mcpb` in the repo root. You can then publish it with Smithery:
115
+
116
+ ```bash
117
+ smithery mcp publish ./server.mcpb -n multimodal-rag/calypso-mcp-server
118
+ ```
119
+
120
+ To publish the bundle together with the capabilities metadata used by the Smithery UI:
121
+
122
+ ```bash
123
+ SMITHERY_API_KEY=... npm run publish:smithery -- --name multimodal-rag/calypso-mcp-server
124
+ ```
125
+
126
+ That command rebuilds `server.mcpb` and uploads it with the server card from `smithery.server-card.json`, so Smithery can render the `calypso-rag-agent` capability with its input schema.
127
+
128
+ ## Publish to npm
129
+
130
+ This package is intended to be published publicly as:
131
+
132
+ ```bash
133
+ @calypso-rag/calypso-mcp
134
+ ```
135
+
136
+ Recommended release flow:
137
+
138
+ ```bash
139
+ npm login
140
+ npm whoami
141
+ npm run build
142
+ npm publish --access public
143
+ ```
144
+
145
+ After publishing, clients can launch it with:
146
+
147
+ ```bash
148
+ npx -y @calypso-rag/calypso-mcp
149
+ ```
150
+
151
+ ## Troubleshooting
152
+
153
+ - **Missing API key**: provide `--api-key` or `CALYPSO_API_KEY`
154
+ - **Wrong API host**: make sure `--api-base-url` / `CALYPSO_API_BASE_URL` ends in `/v1`
155
+ - **Self-hosted deployment**: only override the base URL if you are not using `https://api.calypso.so/v1`
156
+ - **Smithery launch mismatch**: use the packaged `npx -y @calypso-rag/calypso-mcp` path instead of running `node dist/index.js` from a fresh clone
157
+ - **Missing `server.mcpb`**: run `npm run build:mcpb` before calling `smithery mcp publish`
158
+
159
+ ## Available tools
160
+
161
+ ### `calypso-rag-agent`
162
+ Direct Calypso RAG agent access.
163
+
164
+ Notes:
165
+ - It does not auto-route to other personas or agents.
166
+ - It uses `POST /v1/responses` instead of `POST /v1/chat/completions`.
167
+ - First turns create a named conversation, and follow-up turns chain with `previous_response_id`.
168
+ - Optional `fileIds` are attached as `input_file` parts and use `metadata._aicore.file_input_strategy = "rag_policy"` for retrieval-backed agent-store semantics.
169
+ - Use `/new` as the prompt to reset the MCP conversation.
170
+
171
+ ### `calypso-upload-agent-file`
172
+ Uploads a file into the agent store and returns a compatible OpenAI-style `file_id`.
173
+
174
+ Notes:
175
+ - Sends `purpose=user_data` on the upload request.
176
+ - Sends `target_model` so the file lands in the intended agent store instead of a generic attachment path.
177
+ - Supports either `contentBase64` for remote execution or `filePath` for local desktop usage.
178
+ - Can optionally wait until the file is RAG-ready before returning.
179
+
180
+ ### `calypso-upload-knowledge-file`
181
+ Uploads a file into the durable knowledge store and indexing pipeline.
182
+
183
+ Notes:
184
+ - Uses `POST /v1/knowledge/files`.
185
+ - Returns knowledge-file and task metadata, not a chat attachment `file_id`.
186
+ - Supports optional `title`, `tags`, `metadata`, and `idempotencyKey`.
187
+ - Can optionally wait until indexing reaches a ready state before returning.
188
+
189
+ ## Common workflows (copy/paste)
190
+
191
+ ### Knowledge retrieval
192
+
193
+ - **Summarize a topic**:
194
+ - `Summarize the knowledge base guidance for campaign approvals`
195
+ - **Ask for a specific answer**:
196
+ - `What does our documentation say about indexing retries?`
197
+ - **Compare two concepts**:
198
+ - `Compare file indexing with retrieval execution in the current architecture`
199
+ - **Start a fresh thread**:
200
+ - `/new`
201
+
202
+ ### Multi-turn follow-up
203
+
204
+ - **Refine a previous answer**:
205
+ - `Focus only on the ingestion path and ignore retrieval`
206
+ - **Ask for sources or justification**:
207
+ - `Explain which documented components are involved and why`
208
+
209
+ ### Agent-store file flow
210
+
211
+ - **Upload a file for the RAG agent**:
212
+ - Call `calypso-upload-agent-file` with `filename`, `mimeType`, and either `contentBase64` or `filePath`
213
+ - **Ask over the uploaded file**:
214
+ - Call `calypso-rag-agent` with your `prompt` and the returned `fileIds`
215
+ - **RAG semantics**:
216
+ - The MCP automatically uses `rag_policy` when `fileIds` are attached
217
+
218
+ ### Knowledge-store file flow
219
+
220
+ - **Upload durable knowledge**:
221
+ - Call `calypso-upload-knowledge-file` with the file payload and optional `title`, `tags`, or `metadata`
222
+ - **Wait for indexing**:
223
+ - Pass `waitForIndexing: true` if you want the tool to block until the knowledge file is indexed
224
+
225
+ ## Tips
226
+
227
+ - **Start over**: use `/new` to reset the MCP conversation (new `conversation_id` + cleared response chain).
package/dist/config.js ADDED
@@ -0,0 +1,89 @@
1
+ import { z } from "zod";
2
+ export const CALYPSO_RAG_AGENT = "calypso-rag-agent";
3
+ export const CALYPSO_UPLOAD_AGENT_FILE = "calypso-upload-agent-file";
4
+ export const CALYPSO_UPLOAD_KNOWLEDGE_FILE = "calypso-upload-knowledge-file";
5
+ export const DEFAULT_CALYPSO_API_BASE_URL = "https://api.calypso.so/v1";
6
+ const optionalString = z
7
+ .string()
8
+ .trim()
9
+ .transform((value) => value || undefined)
10
+ .optional();
11
+ export const calypsoConfigSchema = z.object({
12
+ apiKey: optionalString,
13
+ apiBaseUrl: optionalString
14
+ .pipe(z.string().url().endsWith("/v1").optional())
15
+ .default(DEFAULT_CALYPSO_API_BASE_URL),
16
+ });
17
+ const CLI_FLAG_ALIASES = {
18
+ "api-key": "apiKey",
19
+ "calypso-api-key": "apiKey",
20
+ "api-base-url": "apiBaseUrl",
21
+ "calypso-api-base-url": "apiBaseUrl",
22
+ };
23
+ function normalizeOptionalValue(value) {
24
+ const trimmed = (value || "").trim();
25
+ return trimmed || undefined;
26
+ }
27
+ function readFlagValue(flag, argv, index) {
28
+ if (flag.includes("=")) {
29
+ const [, rawValue = ""] = flag.split(/=(.*)/s, 2);
30
+ return { value: rawValue, consumedNextArg: false };
31
+ }
32
+ const nextArg = argv[index + 1];
33
+ if (!nextArg || nextArg.startsWith("--")) {
34
+ throw new Error(`Missing value for --${flag.replace(/^--/, "")}`);
35
+ }
36
+ return { value: nextArg, consumedNextArg: true };
37
+ }
38
+ export function parseCliOptions(argv) {
39
+ const options = {
40
+ help: false,
41
+ version: false,
42
+ };
43
+ for (let index = 0; index < argv.length; index += 1) {
44
+ const argument = argv[index];
45
+ if (argument === "--help" || argument === "-h") {
46
+ options.help = true;
47
+ continue;
48
+ }
49
+ if (argument === "--version" || argument === "-v") {
50
+ options.version = true;
51
+ continue;
52
+ }
53
+ if (!argument.startsWith("--")) {
54
+ throw new Error(`Unknown argument: ${argument}`);
55
+ }
56
+ const normalizedFlag = argument.slice(2).split("=")[0];
57
+ const targetOption = CLI_FLAG_ALIASES[normalizedFlag];
58
+ if (!targetOption) {
59
+ throw new Error(`Unknown argument: ${argument}`);
60
+ }
61
+ const { value, consumedNextArg } = readFlagValue(argument, argv, index);
62
+ options[targetOption] = normalizeOptionalValue(value);
63
+ if (consumedNextArg) {
64
+ index += 1;
65
+ }
66
+ }
67
+ return options;
68
+ }
69
+ export function resolveRuntimeConfig(options) {
70
+ return calypsoConfigSchema.parse({
71
+ apiKey: normalizeOptionalValue(options.cli.apiKey) ?? normalizeOptionalValue(options.env.CALYPSO_API_KEY),
72
+ apiBaseUrl: normalizeOptionalValue(options.cli.apiBaseUrl) ?? normalizeOptionalValue(options.env.CALYPSO_API_BASE_URL),
73
+ });
74
+ }
75
+ export function formatUsage(command = "calypso-mcp") {
76
+ return [
77
+ `Usage: ${command} [options]`,
78
+ "",
79
+ "Options:",
80
+ " --api-key <value> Calypso API key",
81
+ " --api-base-url <value> Calypso OpenAI-compatible base URL (must end in /v1)",
82
+ " --help Show help",
83
+ " --version Show version",
84
+ "",
85
+ "Environment variables:",
86
+ " CALYPSO_API_KEY Required for upload/query tool calls",
87
+ ` CALYPSO_API_BASE_URL Optional, defaults to ${DEFAULT_CALYPSO_API_BASE_URL}`,
88
+ ].join("\n");
89
+ }
package/dist/files.js ADDED
@@ -0,0 +1,244 @@
1
+ import { readFile } from "fs/promises";
2
+ import path from "path";
3
+ const DEFAULT_MIME_TYPE = "application/octet-stream";
4
+ const OPENAI_READY_STATE = "active";
5
+ const KNOWLEDGE_READY_STATE = "indexed";
6
+ const KNOWLEDGE_ERROR_STATES = new Set(["failed", "deleted"]);
7
+ const OPENAI_POLL_MAX_ATTEMPTS = 40;
8
+ const KNOWLEDGE_POLL_MAX_ATTEMPTS = 40;
9
+ const POLL_INITIAL_DELAY_MS = 1000;
10
+ const POLL_MAX_DELAY_MS = 4000;
11
+ const POLL_BACKOFF_MULTIPLIER = 1.5;
12
+ function buildApiUrl(config, relativePath) {
13
+ const normalizedPath = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
14
+ return new URL(normalizedPath, `${config.apiBaseUrl}/`).toString();
15
+ }
16
+ function requireApiKey(config) {
17
+ const apiKey = String(config.apiKey || "").trim();
18
+ if (!apiKey) {
19
+ throw new Error("CALYPSO_API_KEY is required to call Calypso tools, but it is not configured.");
20
+ }
21
+ return apiKey;
22
+ }
23
+ async function parseResponseBody(response) {
24
+ const contentType = response.headers.get("content-type") || "";
25
+ if (contentType.includes("application/json")) {
26
+ return response.json();
27
+ }
28
+ const text = await response.text();
29
+ if (!text) {
30
+ return null;
31
+ }
32
+ try {
33
+ return JSON.parse(text);
34
+ }
35
+ catch {
36
+ return text;
37
+ }
38
+ }
39
+ function formatApiError(status, body) {
40
+ if (body && typeof body === "object") {
41
+ const maybeError = "error" in body && typeof body.error === "string"
42
+ ? body.error
43
+ : "message" in body && typeof body.message === "string"
44
+ ? body.message
45
+ : null;
46
+ if (maybeError) {
47
+ return `Request failed with status ${status}: ${maybeError}`;
48
+ }
49
+ }
50
+ if (typeof body === "string" && body.trim()) {
51
+ return `Request failed with status ${status}: ${body.trim()}`;
52
+ }
53
+ return `Request failed with status ${status}`;
54
+ }
55
+ async function requestJson(config, relativePath, init) {
56
+ const headers = new Headers(init.headers);
57
+ headers.set("Authorization", `Bearer ${requireApiKey(config)}`);
58
+ const response = await fetch(buildApiUrl(config, relativePath), {
59
+ ...init,
60
+ headers,
61
+ });
62
+ const body = await parseResponseBody(response);
63
+ if (!response.ok) {
64
+ throw new Error(formatApiError(response.status, body));
65
+ }
66
+ return body;
67
+ }
68
+ function stripDataUriPrefix(value) {
69
+ const marker = ";base64,";
70
+ const markerIndex = value.indexOf(marker);
71
+ if (markerIndex === -1) {
72
+ return value;
73
+ }
74
+ return value.slice(markerIndex + marker.length);
75
+ }
76
+ async function resolveUploadContent(input) {
77
+ const hasContentBase64 = typeof input.contentBase64 === "string" && input.contentBase64.trim().length > 0;
78
+ const hasFilePath = typeof input.filePath === "string" && input.filePath.trim().length > 0;
79
+ if (hasContentBase64 === hasFilePath) {
80
+ throw new Error("Provide exactly one of `contentBase64` or `filePath`.");
81
+ }
82
+ const filename = (input.filename || "").trim() || (hasFilePath && input.filePath ? path.basename(input.filePath) : "");
83
+ if (!filename) {
84
+ throw new Error("A filename is required.");
85
+ }
86
+ const mimeType = (input.mimeType || "").trim() || DEFAULT_MIME_TYPE;
87
+ if (hasFilePath) {
88
+ const bytes = await readFile(String(input.filePath));
89
+ return {
90
+ bytes,
91
+ filename,
92
+ mimeType,
93
+ };
94
+ }
95
+ const normalizedBase64 = stripDataUriPrefix(String(input.contentBase64).trim());
96
+ const bytes = Buffer.from(normalizedBase64, "base64");
97
+ if (bytes.byteLength === 0) {
98
+ throw new Error("The provided `contentBase64` value could not be decoded.");
99
+ }
100
+ return {
101
+ bytes,
102
+ filename,
103
+ mimeType,
104
+ };
105
+ }
106
+ function createMultipartFile(content) {
107
+ return new Blob([content.bytes], { type: content.mimeType || DEFAULT_MIME_TYPE });
108
+ }
109
+ async function sleep(ms) {
110
+ await new Promise((resolve) => setTimeout(resolve, ms));
111
+ }
112
+ function isOpenAiFileReady(file) {
113
+ return file.metadata?.rag_readiness?.state === OPENAI_READY_STATE || file.metadata?.rag_readiness?.is_ready === true;
114
+ }
115
+ function getOpenAiFileError(file) {
116
+ if (file.metadata?.rag_readiness?.state === "error") {
117
+ return String(file.metadata.rag_readiness.detail || file.metadata.rag_readiness.label || "File processing failed.");
118
+ }
119
+ if (file.status === "error") {
120
+ return "The uploaded file entered an error state.";
121
+ }
122
+ return null;
123
+ }
124
+ function extractKnowledgeStatus(result) {
125
+ return String(result.file.status || result.task?.status || "").trim().toLowerCase();
126
+ }
127
+ function isKnowledgeReady(result) {
128
+ return extractKnowledgeStatus(result) === KNOWLEDGE_READY_STATE;
129
+ }
130
+ function getKnowledgeError(result) {
131
+ const status = extractKnowledgeStatus(result);
132
+ if (!status) {
133
+ return null;
134
+ }
135
+ if (KNOWLEDGE_ERROR_STATES.has(status)) {
136
+ return `Knowledge indexing entered terminal status \`${status}\`.`;
137
+ }
138
+ return null;
139
+ }
140
+ export async function getOpenAiFile(config, fileId) {
141
+ return requestJson(config, `/files/${encodeURIComponent(fileId)}`, {
142
+ method: "GET",
143
+ });
144
+ }
145
+ export async function waitForOpenAiFileReady(config, fileId) {
146
+ let delayMs = POLL_INITIAL_DELAY_MS;
147
+ for (let attempt = 0; attempt < OPENAI_POLL_MAX_ATTEMPTS; attempt += 1) {
148
+ const file = await getOpenAiFile(config, fileId);
149
+ const readinessError = getOpenAiFileError(file);
150
+ if (readinessError) {
151
+ throw new Error(readinessError);
152
+ }
153
+ if (isOpenAiFileReady(file)) {
154
+ return file;
155
+ }
156
+ if (attempt < OPENAI_POLL_MAX_ATTEMPTS - 1) {
157
+ await sleep(delayMs);
158
+ delayMs = Math.min(POLL_MAX_DELAY_MS, Math.round(delayMs * POLL_BACKOFF_MULTIPLIER));
159
+ }
160
+ }
161
+ throw new Error("Timed out while waiting for the uploaded file to become RAG-ready.");
162
+ }
163
+ export async function uploadAgentFile(config, params) {
164
+ const content = await resolveUploadContent(params);
165
+ const form = new FormData();
166
+ form.set("purpose", "user_data");
167
+ form.set("target_model", params.targetModel);
168
+ form.set("file", createMultipartFile(content), content.filename);
169
+ const uploaded = await requestJson(config, "/files", {
170
+ method: "POST",
171
+ body: form,
172
+ });
173
+ if (params.waitForReady === false) {
174
+ return uploaded;
175
+ }
176
+ return waitForOpenAiFileReady(config, uploaded.id);
177
+ }
178
+ export async function getKnowledgeFile(config, fileId) {
179
+ return requestJson(config, `/knowledge/files/${encodeURIComponent(fileId)}`, {
180
+ method: "GET",
181
+ });
182
+ }
183
+ export async function getKnowledgeTask(config, taskId) {
184
+ return requestJson(config, `/knowledge/tasks/${encodeURIComponent(taskId)}`, {
185
+ method: "GET",
186
+ });
187
+ }
188
+ async function resolveKnowledgeResult(config, fileId, taskId) {
189
+ const file = await getKnowledgeFile(config, fileId);
190
+ const task = taskId ? await getKnowledgeTask(config, taskId) : file.task || null;
191
+ return { file, task };
192
+ }
193
+ export async function waitForKnowledgeFileIndexed(config, fileId, taskId) {
194
+ let delayMs = POLL_INITIAL_DELAY_MS;
195
+ for (let attempt = 0; attempt < KNOWLEDGE_POLL_MAX_ATTEMPTS; attempt += 1) {
196
+ const result = await resolveKnowledgeResult(config, fileId, taskId);
197
+ const readinessError = getKnowledgeError(result);
198
+ if (readinessError) {
199
+ throw new Error(readinessError);
200
+ }
201
+ if (isKnowledgeReady(result)) {
202
+ return result;
203
+ }
204
+ if (attempt < KNOWLEDGE_POLL_MAX_ATTEMPTS - 1) {
205
+ await sleep(delayMs);
206
+ delayMs = Math.min(POLL_MAX_DELAY_MS, Math.round(delayMs * POLL_BACKOFF_MULTIPLIER));
207
+ }
208
+ }
209
+ throw new Error("Timed out while waiting for the knowledge file to finish indexing.");
210
+ }
211
+ export async function uploadKnowledgeFile(config, params) {
212
+ const content = await resolveUploadContent(params);
213
+ const form = new FormData();
214
+ form.set("file", createMultipartFile(content), content.filename);
215
+ if (params.title?.trim()) {
216
+ form.set("title", params.title.trim());
217
+ }
218
+ for (const tag of params.tags || []) {
219
+ const trimmedTag = String(tag || "").trim();
220
+ if (trimmedTag) {
221
+ form.append("tags", trimmedTag);
222
+ }
223
+ }
224
+ if (params.metadata && Object.keys(params.metadata).length > 0) {
225
+ form.set("metadata", JSON.stringify(params.metadata));
226
+ }
227
+ const headers = new Headers();
228
+ if (params.idempotencyKey?.trim()) {
229
+ headers.set("Idempotency-Key", params.idempotencyKey.trim());
230
+ }
231
+ const file = await requestJson(config, "/knowledge/files", {
232
+ method: "POST",
233
+ headers,
234
+ body: form,
235
+ });
236
+ const initialResult = {
237
+ file,
238
+ task: file.task || null,
239
+ };
240
+ if (params.waitForIndexing !== true) {
241
+ return initialResult;
242
+ }
243
+ return waitForKnowledgeFileIndexed(config, file.id, file.task?.id || null);
244
+ }
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import dotenv from "dotenv";
4
+ import { readFile } from "fs/promises";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { DEFAULT_CALYPSO_API_BASE_URL, formatUsage, parseCliOptions, resolveRuntimeConfig } from "./config.js";
8
+ import { createCalypsoMcpServer } from "./server.js";
9
+ const FALLBACK_PACKAGE_INFO = {
10
+ name: "@calypso-rag/calypso-mcp",
11
+ version: "0.0.0",
12
+ };
13
+ async function loadPackageInfo() {
14
+ try {
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const packageJsonPath = path.join(__dirname, "..", "package.json");
18
+ const packageJsonContent = await readFile(packageJsonPath, "utf8");
19
+ const packageJson = JSON.parse(packageJsonContent);
20
+ return {
21
+ name: packageJson.name || FALLBACK_PACKAGE_INFO.name,
22
+ version: packageJson.version || FALLBACK_PACKAGE_INFO.version,
23
+ };
24
+ }
25
+ catch {
26
+ return FALLBACK_PACKAGE_INFO;
27
+ }
28
+ }
29
+ // Start the server with stdio transport
30
+ async function main() {
31
+ try {
32
+ dotenv.config();
33
+ const packageInfo = await loadPackageInfo();
34
+ const cliOptions = parseCliOptions(process.argv.slice(2));
35
+ if (cliOptions.help) {
36
+ console.log(formatUsage(packageInfo.name));
37
+ process.exit(0);
38
+ }
39
+ if (cliOptions.version) {
40
+ console.log(packageInfo.version);
41
+ process.exit(0);
42
+ }
43
+ const runtimeConfig = resolveRuntimeConfig({
44
+ cli: cliOptions,
45
+ env: process.env,
46
+ });
47
+ const server = createCalypsoMcpServer({
48
+ config: runtimeConfig,
49
+ packageInfo,
50
+ });
51
+ const transport = new StdioServerTransport();
52
+ await server.connect(transport);
53
+ }
54
+ catch (error) {
55
+ if (error instanceof Error) {
56
+ console.error(`Error: ${error.message}`);
57
+ console.error(`Example: env CALYPSO_API_KEY=sk-... CALYPSO_API_BASE_URL=${DEFAULT_CALYPSO_API_BASE_URL} npx -y @calypso-rag/calypso-mcp`);
58
+ console.error("");
59
+ console.error(formatUsage());
60
+ }
61
+ process.exit(1);
62
+ }
63
+ }
64
+ main();
package/dist/server.js ADDED
@@ -0,0 +1,276 @@
1
+ import { randomUUID } from "crypto";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import OpenAI from "openai";
4
+ import { z } from "zod";
5
+ import { CALYPSO_RAG_AGENT, CALYPSO_UPLOAD_AGENT_FILE, CALYPSO_UPLOAD_KNOWLEDGE_FILE, } from "./config.js";
6
+ import { uploadAgentFile, uploadKnowledgeFile } from "./files.js";
7
+ async function processStreamingResponse(stream) {
8
+ let fullResponse = "";
9
+ let responseId = null;
10
+ for await (const event of stream) {
11
+ if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
12
+ fullResponse += event.delta;
13
+ }
14
+ if (event.type === "response.output_text.done" && !fullResponse && typeof event.text === "string") {
15
+ fullResponse = event.text;
16
+ }
17
+ if (event.type === "response.completed" && typeof event.response?.id === "string") {
18
+ responseId = event.response.id;
19
+ }
20
+ }
21
+ return { text: fullResponse, responseId };
22
+ }
23
+ function formatJson(value) {
24
+ return JSON.stringify(value, null, 2);
25
+ }
26
+ function normalizeFileIds(fileIds) {
27
+ const normalized = (fileIds || [])
28
+ .map((fileId) => String(fileId || "").trim())
29
+ .filter(Boolean);
30
+ return normalized.length > 0 ? normalized : undefined;
31
+ }
32
+ function buildResponsesMetadata(options) {
33
+ const metadata = {
34
+ tool: "mcp",
35
+ agent: CALYPSO_RAG_AGENT,
36
+ conversation_id: options.conversationId,
37
+ };
38
+ if (options.fileIds && options.fileIds.length > 0) {
39
+ metadata._aicore = {
40
+ file_input_strategy: "rag_policy",
41
+ };
42
+ }
43
+ return metadata;
44
+ }
45
+ function requireApiKey(config) {
46
+ const apiKey = String(config.apiKey || "").trim();
47
+ if (!apiKey) {
48
+ throw new Error("CALYPSO_API_KEY is required to call Calypso tools, but it is not configured.");
49
+ }
50
+ return apiKey;
51
+ }
52
+ export function createCalypsoMcpServer(options) {
53
+ const { config, packageInfo } = options;
54
+ let calypsoClient = null;
55
+ function getCalypsoClient() {
56
+ if (!calypsoClient) {
57
+ calypsoClient = new OpenAI({
58
+ apiKey: requireApiKey(config),
59
+ baseURL: config.apiBaseUrl,
60
+ defaultHeaders: {
61
+ "User-Agent": `${packageInfo.name}/${packageInfo.version} (Node.js/${process.versions.node})`,
62
+ },
63
+ });
64
+ }
65
+ return calypsoClient;
66
+ }
67
+ const server = new McpServer({
68
+ name: packageInfo.name,
69
+ version: packageInfo.version,
70
+ });
71
+ // MCP session state is intentionally per-process. The backend maintains the
72
+ // real conversation thread through previous_response_id chaining.
73
+ let conversationId = `conv_${randomUUID().replace(/-/g, "")}`;
74
+ let previousResponseId = null;
75
+ server.tool(CALYPSO_UPLOAD_AGENT_FILE, [
76
+ "[CALYPSO UPLOAD AGENT FILE]",
77
+ "Uploads a file into the agent store for retrieval-backed RAG use.",
78
+ "",
79
+ "Use this when you want a compatible `file_id` that can be attached to `calypso-rag-agent`.",
80
+ "The MCP sends `purpose=user_data`, targets the selected RAG agent with `target_model`,",
81
+ "and can optionally wait until the file is RAG-ready before returning.",
82
+ ].join("\n"), {
83
+ filename: z.string().describe("Display filename for the uploaded file."),
84
+ mimeType: z.string().describe("Content type for the uploaded file."),
85
+ contentBase64: z.string().optional().describe("Base64-encoded file content. Use this for Smithery or remote execution."),
86
+ filePath: z.string().optional().describe("Local file path to read from disk when the MCP process can access the file."),
87
+ targetModel: z.string().optional().describe("Optional RAG agent id. Defaults to `calypso-rag-agent`."),
88
+ waitForReady: z.boolean().optional().describe("If true, wait until the uploaded file is RAG-ready before returning."),
89
+ }, async ({ filename, mimeType, contentBase64, filePath, targetModel, waitForReady, }) => {
90
+ try {
91
+ const uploaded = await uploadAgentFile(config, {
92
+ filename,
93
+ mimeType,
94
+ contentBase64,
95
+ filePath,
96
+ targetModel: String(targetModel || "").trim() || CALYPSO_RAG_AGENT,
97
+ waitForReady,
98
+ });
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: formatJson(uploaded),
104
+ },
105
+ ],
106
+ };
107
+ }
108
+ catch (error) {
109
+ console.error(`Error calling ${CALYPSO_UPLOAD_AGENT_FILE}:`, error);
110
+ return {
111
+ isError: true,
112
+ content: [
113
+ {
114
+ type: "text",
115
+ text: `Error: Failed to upload file into the agent store. ${error}`,
116
+ },
117
+ ],
118
+ };
119
+ }
120
+ });
121
+ server.tool(CALYPSO_UPLOAD_KNOWLEDGE_FILE, [
122
+ "[CALYPSO UPLOAD KNOWLEDGE FILE]",
123
+ "Uploads a file into the durable knowledge store and indexing pipeline.",
124
+ "",
125
+ "Use this when you want a file indexed into the broader knowledge corpus instead of",
126
+ "attached directly to a single RAG chat turn. This tool returns knowledge-file and task metadata.",
127
+ ].join("\n"), {
128
+ filename: z.string().describe("Display filename for the uploaded knowledge file."),
129
+ mimeType: z.string().describe("Content type for the uploaded knowledge file."),
130
+ contentBase64: z.string().optional().describe("Base64-encoded file content. Use this for Smithery or remote execution."),
131
+ filePath: z.string().optional().describe("Local file path to read from disk when the MCP process can access the file."),
132
+ title: z.string().optional().describe("Optional human-readable title stored with the knowledge file."),
133
+ tags: z.array(z.string()).optional().describe("Optional tags for knowledge-store organization."),
134
+ metadata: z.record(z.unknown()).optional().describe("Optional metadata object serialized onto the upload request."),
135
+ idempotencyKey: z.string().optional().describe("Optional idempotency key for durable upload retries."),
136
+ waitForIndexing: z.boolean().optional().describe("If true, wait until indexing reaches a terminal ready state before returning."),
137
+ }, async ({ filename, mimeType, contentBase64, filePath, title, tags, metadata, idempotencyKey, waitForIndexing, }) => {
138
+ try {
139
+ const result = await uploadKnowledgeFile(config, {
140
+ filename,
141
+ mimeType,
142
+ contentBase64,
143
+ filePath,
144
+ title,
145
+ tags,
146
+ metadata,
147
+ idempotencyKey,
148
+ waitForIndexing,
149
+ });
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: formatJson(result),
155
+ },
156
+ ],
157
+ };
158
+ }
159
+ catch (error) {
160
+ console.error(`Error calling ${CALYPSO_UPLOAD_KNOWLEDGE_FILE}:`, error);
161
+ return {
162
+ isError: true,
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: `Error: Failed to upload file into the knowledge store. ${error}`,
167
+ },
168
+ ],
169
+ };
170
+ }
171
+ });
172
+ server.tool(CALYPSO_RAG_AGENT, [
173
+ "[CALYPSO RAG AGENT]",
174
+ "Sends each prompt directly to the Calypso RAG agent using the full conversation context.",
175
+ "",
176
+ "Use this when you want Calypso knowledge retrieval and grounded answers from the RAG backend.",
177
+ "Typical requests:",
178
+ '- "Summarize the key points from our onboarding documentation"',
179
+ '- "What does the knowledge base say about campaign approval rules?"',
180
+ '- "Compare the documented indexing flow with the retrieval flow"',
181
+ '- "Answer using the uploaded file ids: [\\"file_123\\"]"',
182
+ "",
183
+ "Responses API behavior:",
184
+ "- First turns start a named Calypso conversation via `/v1/responses`.",
185
+ "- Follow-up turns chain with `previous_response_id` so the backend owns conversation state.",
186
+ "- When `fileIds` are provided, the MCP uses `rag_policy` retrieval semantics instead of inline attachment stuffing.",
187
+ "",
188
+ "MCP session behavior:",
189
+ "- This tool maintains a stable conversation id in the background for multi-turn retrieval context.",
190
+ "- Use `/new` to start a fresh conversation and clear the current context window.",
191
+ "",
192
+ "Quick commands (examples):",
193
+ '- "Summarize the latest indexed knowledge about WhatsApp templates"',
194
+ '- "Find the source of truth for campaign approval behavior"',
195
+ '- "Start a new topic" (or use `/new`)',
196
+ ].join("\n"), {
197
+ prompt: z.string().describe("Your request. Include context, constraints, and desired output."),
198
+ fileIds: z.array(z.string()).optional().describe("Optional uploaded agent-store `file_id` values to attach with `rag_policy` retrieval semantics."),
199
+ }, async ({ prompt, fileIds }) => {
200
+ try {
201
+ const userText = (prompt || "").trim();
202
+ const normalizedFileIds = normalizeFileIds(fileIds);
203
+ if (userText === "/new") {
204
+ conversationId = `conv_${randomUUID().replace(/-/g, "")}`;
205
+ previousResponseId = null;
206
+ return {
207
+ content: [
208
+ {
209
+ type: "text",
210
+ text: "Started a new Calypso RAG conversation. You can continue with your next request.",
211
+ },
212
+ ],
213
+ };
214
+ }
215
+ const request = {
216
+ model: CALYPSO_RAG_AGENT,
217
+ input: [
218
+ {
219
+ role: "user",
220
+ content: [
221
+ {
222
+ type: "input_text",
223
+ text: userText,
224
+ },
225
+ ...(normalizedFileIds || []).map((fileId) => ({
226
+ type: "input_file",
227
+ file_id: fileId,
228
+ })),
229
+ ],
230
+ },
231
+ ],
232
+ stream: true,
233
+ store: true,
234
+ metadata: buildResponsesMetadata({
235
+ conversationId,
236
+ fileIds: normalizedFileIds,
237
+ }),
238
+ };
239
+ if (previousResponseId) {
240
+ request.previous_response_id = previousResponseId;
241
+ }
242
+ else {
243
+ request.conversation = { id: conversationId };
244
+ }
245
+ // Calypso/AIcore accepts Responses fields that this SDK version does not
246
+ // type yet (`conversation` and `previous_response_id`), so we narrow the
247
+ // cast to the API boundary.
248
+ const response = await getCalypsoClient().responses.create(request);
249
+ const result = await processStreamingResponse(response);
250
+ if (result.responseId) {
251
+ previousResponseId = result.responseId;
252
+ }
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: result.text,
258
+ },
259
+ ],
260
+ };
261
+ }
262
+ catch (error) {
263
+ console.error(`Error calling ${CALYPSO_RAG_AGENT}:`, error);
264
+ return {
265
+ isError: true,
266
+ content: [
267
+ {
268
+ type: "text",
269
+ text: `Error: Failed to process ${CALYPSO_RAG_AGENT} query. ${error}`,
270
+ },
271
+ ],
272
+ };
273
+ }
274
+ });
275
+ return server;
276
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@calypso-rag/calypso-mcp",
3
+ "version": "1.0.23",
4
+ "description": "MCP server for Calypso (AIcore) agents via OpenAI-compatible API.",
5
+ "main": "dist/index.js",
6
+ "homepage": "https://github.com/calypso-so/calypso-mcp-server",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/calypso-so/calypso-mcp-server.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/calypso-so/calypso-mcp-server/issues"
13
+ },
14
+ "type": "module",
15
+ "bin": {
16
+ "calypso-mcp": "dist/index.js"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
26
+ "typecheck": "tsc --noEmit",
27
+ "validate:mcpb": "mcpb validate manifest.json",
28
+ "build:mcpb": "npm run build && npm run validate:mcpb && mcpb pack . server.mcpb",
29
+ "smoke:stdio": "node \"scripts/smoke-stdio.mjs\"",
30
+ "validate:smithery": "npm run build && npm run smoke:stdio && npm run validate:mcpb",
31
+ "publish:smithery": "npm run build:mcpb && node \"scripts/publish-smithery.mjs\"",
32
+ "test": "echo \"No tests specified\"",
33
+ "start": "node dist/index.js",
34
+ "lint": "echo \"No linting configured\"",
35
+ "format": "echo \"No formatting configured\"",
36
+ "prepare": "npm run build",
37
+ "release": "npm run build && npm publish"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "calypso",
42
+ "aicore",
43
+ "smithery",
44
+ "rag",
45
+ "responses-api",
46
+ "agents",
47
+ "openai-compatible"
48
+ ],
49
+ "author": "Calypso",
50
+ "license": "MIT",
51
+ "dependencies": {
52
+ "@modelcontextprotocol/sdk": "^1.0.0",
53
+ "dotenv": "^16.3.1",
54
+ "openai": "^4.20.1",
55
+ "zod": "^3.22.4"
56
+ },
57
+ "devDependencies": {
58
+ "@anthropic-ai/mcpb": "^2.1.2",
59
+ "@smithery/api": "^0.66.0",
60
+ "@types/node": "^20.10.0",
61
+ "ts-node": "^10.9.2",
62
+ "typescript": "^5.3.2"
63
+ },
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ }
67
+ }