@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 +21 -0
- package/README.md +227 -0
- package/dist/config.js +89 -0
- package/dist/files.js +244 -0
- package/dist/index.js +64 -0
- package/dist/server.js +276 -0
- package/package.json +67 -0
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
|
+
[](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
|
+
}
|