@bywaleed/onyx-mcp 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/LICENSE +21 -0
- package/README.md +151 -0
- package/SECURITY.md +30 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +155 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +49 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +16 -0
- package/dist/register.d.ts +11 -0
- package/dist/register.js +20 -0
- package/dist/request-context.d.ts +2 -0
- package/dist/request-context.js +8 -0
- package/dist/schemas.d.ts +38 -0
- package/dist/schemas.js +25 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +29 -0
- package/dist/tool-helpers.d.ts +4 -0
- package/dist/tool-helpers.js +21 -0
- package/dist/tools/admin.d.ts +2 -0
- package/dist/tools/admin.js +56 -0
- package/dist/tools/chat.d.ts +2 -0
- package/dist/tools/chat.js +98 -0
- package/dist/tools/projects.d.ts +2 -0
- package/dist/tools/projects.js +51 -0
- package/dist/tools/raw.d.ts +2 -0
- package/dist/tools/raw.js +35 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +49 -0
- package/dist/tools/system.d.ts +2 -0
- package/dist/tools/system.js +30 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +83 -0
- package/server.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waleed Islam
|
|
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,151 @@
|
|
|
1
|
+
# Onyx MCP
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ByWaleed/onyx-mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
MCP Registry name: `io.github.bywaleed/onyx-mcp`
|
|
7
|
+
|
|
8
|
+
A comprehensive, secure Model Context Protocol server for [Onyx](https://www.onyx.app/), formerly Danswer.
|
|
9
|
+
|
|
10
|
+
This project exposes Onyx search, chat, agents, projects, documents, connectors, ingestion, and deployment-specific APIs to MCP clients. It is an independent community project and is not an official Onyx package.
|
|
11
|
+
|
|
12
|
+
## Why This Server
|
|
13
|
+
|
|
14
|
+
- Uses the current `/chat/send-chat-message` contract.
|
|
15
|
+
- Defaults to Onyx's built-in assistant, persona `0`.
|
|
16
|
+
- Supports scoped Personal Access Tokens and legacy API keys.
|
|
17
|
+
- Starts read-only.
|
|
18
|
+
- Separately gates writes, administration, destructive actions, and raw API access.
|
|
19
|
+
- Applies request timeouts and response-size limits.
|
|
20
|
+
- Never logs authorization headers or tokens.
|
|
21
|
+
- Includes an advanced raw API tool for deployment-specific endpoints.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx -y @bywaleed/onyx-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Until the first npm release, clone the repository and run `npm install && npm run build`.
|
|
30
|
+
|
|
31
|
+
Versioned tarballs will be attached to [GitHub Releases](https://github.com/ByWaleed/onyx-mcp/releases) and published to GitHub Packages as `@bywaleed/onyx-mcp`.
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Required:
|
|
36
|
+
|
|
37
|
+
| Variable | Description |
|
|
38
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
39
|
+
| `ONYX_API_URL` | Onyx API base URL, for example `https://onyx.example.com/api` |
|
|
40
|
+
| `ONYX_API_TOKEN` | Onyx PAT or API key |
|
|
41
|
+
|
|
42
|
+
Optional:
|
|
43
|
+
|
|
44
|
+
| Variable | Default | Description |
|
|
45
|
+
| ----------------------------- | --------- | ------------------------------------------------------------------ |
|
|
46
|
+
| `ONYX_DEFAULT_PERSONA_ID` | `0` | Default agent used for new chats |
|
|
47
|
+
| `ONYX_MCP_ENABLE_WRITE` | `false` | Registers tools that create or modify data |
|
|
48
|
+
| `ONYX_MCP_ENABLE_ADMIN` | `false` | Registers administrative tools |
|
|
49
|
+
| `ONYX_MCP_ENABLE_DESTRUCTIVE` | `false` | Registers destructive tools; write must also be enabled |
|
|
50
|
+
| `ONYX_MCP_ENABLE_RAW_API` | `false` | Registers the advanced raw API tool; admin access is also required |
|
|
51
|
+
| `ONYX_MCP_TIMEOUT_MS` | `30000` | Request timeout |
|
|
52
|
+
| `ONYX_MCP_MAX_RESPONSE_BYTES` | `1000000` | Maximum accepted response body |
|
|
53
|
+
| `ONYX_MCP_MAX_CONCURRENCY` | `8` | Maximum concurrent requests to Onyx |
|
|
54
|
+
|
|
55
|
+
Onyx still enforces the permissions attached to the supplied token. Enabling a profile cannot grant additional Onyx privileges.
|
|
56
|
+
|
|
57
|
+
## OpenCode
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcp": {
|
|
62
|
+
"onyx": {
|
|
63
|
+
"type": "local",
|
|
64
|
+
"command": ["npx", "-y", "@bywaleed/onyx-mcp"],
|
|
65
|
+
"environment": {
|
|
66
|
+
"ONYX_API_URL": "https://onyx.example.com/api",
|
|
67
|
+
"ONYX_API_TOKEN": "{env:ONYX_API_TOKEN}"
|
|
68
|
+
},
|
|
69
|
+
"enabled": true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Claude Desktop
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"onyx": {
|
|
81
|
+
"command": "npx",
|
|
82
|
+
"args": ["-y", "@bywaleed/onyx-mcp"],
|
|
83
|
+
"env": {
|
|
84
|
+
"ONYX_API_URL": "https://onyx.example.com/api",
|
|
85
|
+
"ONYX_API_TOKEN": "your-token"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Tool Profiles
|
|
93
|
+
|
|
94
|
+
The read-only profile includes health, identity, permissions, search, chat history, agents, projects, files, tools, document sets, and connector status.
|
|
95
|
+
|
|
96
|
+
The write profile adds chat creation, chat messages, feedback, and project updates.
|
|
97
|
+
|
|
98
|
+
The admin profile adds connector, credential, user, agent, and direct-ingestion administration.
|
|
99
|
+
|
|
100
|
+
The destructive profile adds individually confirmed deletion tools. Bulk deletion is intentionally not exposed as a first-class tool.
|
|
101
|
+
|
|
102
|
+
The raw API profile adds `onyx_api_request`. It covers APIs specific to an Onyx edition or version. Every raw request requires the admin profile. Mutations also require write access, and deletions require destructive access plus confirmation.
|
|
103
|
+
|
|
104
|
+
## Tools
|
|
105
|
+
|
|
106
|
+
The default profile provides health, version, identity, permission, search, agent, chat-history, project, file, document-set, tool, source, and connector-status tools. Write mode adds chat, feedback, and project mutations. Admin mode adds connector, credential, user, agent, and ingestion tools.
|
|
107
|
+
|
|
108
|
+
Run `onyx_capabilities` to inspect the active profile. MCP clients can also call `tools/list` for complete machine-readable schemas and safety annotations.
|
|
109
|
+
|
|
110
|
+
## Authentication
|
|
111
|
+
|
|
112
|
+
Prefer an Onyx PAT with the smallest required scope:
|
|
113
|
+
|
|
114
|
+
- `read:search` for search.
|
|
115
|
+
- `read:chat` for chat history.
|
|
116
|
+
- `write:chat` for chat creation and messages.
|
|
117
|
+
|
|
118
|
+
Some Onyx endpoints still use legacy role checks and need an unrestricted PAT or API key. Use a separate administrative server configuration for those operations.
|
|
119
|
+
|
|
120
|
+
Treat client configuration files as sensitive when they contain a token. Use a dedicated least-privilege token and restrict file permissions to your user account.
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm ci
|
|
126
|
+
npm run verify
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Compatibility
|
|
130
|
+
|
|
131
|
+
| Component | Supported |
|
|
132
|
+
| ---------------- | --------------------------------------------------------------------------------------------------- |
|
|
133
|
+
| Node.js | 20 and 22 |
|
|
134
|
+
| MCP SDK/protocol | TypeScript SDK 1.x, MCP through `2025-11-25` |
|
|
135
|
+
| Onyx | Current community and enterprise APIs; tested against the repository version documented in releases |
|
|
136
|
+
| Transport | Local stdio |
|
|
137
|
+
|
|
138
|
+
The server uses non-streaming Onyx chat responses for a stable MCP result. Onyx editions and releases expose different administrative routes. The raw API tool provides an escape hatch while first-class tools remain curated and safe. MCP SDK 2.x migration is planned as a separate breaking compatibility release.
|
|
139
|
+
|
|
140
|
+
## Support And Security
|
|
141
|
+
|
|
142
|
+
- Usage questions: [GitHub Discussions](https://github.com/ByWaleed/onyx-mcp/discussions)
|
|
143
|
+
- Bugs and feature requests: [GitHub Issues](https://github.com/ByWaleed/onyx-mcp/issues)
|
|
144
|
+
- Vulnerabilities: [private security advisory](https://github.com/ByWaleed/onyx-mcp/security/advisories/new)
|
|
145
|
+
- Release history: [CHANGELOG.md](CHANGELOG.md)
|
|
146
|
+
- Maintainer release process: [RELEASING.md](RELEASING.md)
|
|
147
|
+
- MCP Registry metadata: [server.json](server.json)
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting
|
|
4
|
+
|
|
5
|
+
Report security issues privately through [GitHub Security Advisories](https://github.com/ByWaleed/onyx-mcp/security/advisories/new). Do not create a public issue for a suspected vulnerability.
|
|
6
|
+
|
|
7
|
+
You should receive an acknowledgement within seven days. Resolution time depends on severity and complexity.
|
|
8
|
+
|
|
9
|
+
## Supported Versions
|
|
10
|
+
|
|
11
|
+
| Version | Supported |
|
|
12
|
+
| -------------- | ----------- |
|
|
13
|
+
| Latest release | Yes |
|
|
14
|
+
| Older releases | Best effort |
|
|
15
|
+
|
|
16
|
+
## Credential Safety
|
|
17
|
+
|
|
18
|
+
- Use a scoped Onyx Personal Access Token where possible.
|
|
19
|
+
- Do not commit tokens or put them directly in shared configuration files.
|
|
20
|
+
- Run administrative access as a separate MCP server configuration.
|
|
21
|
+
- Leave destructive and raw API access disabled unless required.
|
|
22
|
+
- Raw API access always requires the administrative profile.
|
|
23
|
+
- Use HTTPS for remote Onyx deployments. Plain HTTP is accepted only for loopback development hosts.
|
|
24
|
+
- Rotate a token immediately if it appears in logs, shell history, or source control.
|
|
25
|
+
|
|
26
|
+
## Safety Model
|
|
27
|
+
|
|
28
|
+
This server has local capability gates, but Onyx is the source of truth for authorization. The server cannot grant access that the configured Onyx token does not have.
|
|
29
|
+
|
|
30
|
+
Destructive first-class tools require both `ONYX_MCP_ENABLE_DESTRUCTIVE=true` and an explicit `confirm: true` argument. Raw DELETE requests have the same requirement.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { createServer } from "./server.js";
|
|
5
|
+
async function main() {
|
|
6
|
+
const server = createServer(loadConfig());
|
|
7
|
+
await server.connect(new StdioServerTransport());
|
|
8
|
+
}
|
|
9
|
+
main().catch((error) => {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
console.error(`Failed to start onyx-mcp: ${message}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Config } from "./config.js";
|
|
2
|
+
export interface RequestOptions {
|
|
3
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
query?: Record<string, unknown>;
|
|
5
|
+
body?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export declare class OnyxClient {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private readonly fetchImpl;
|
|
10
|
+
private activeRequests;
|
|
11
|
+
private readonly waiters;
|
|
12
|
+
constructor(config: Config, fetchImpl?: typeof fetch);
|
|
13
|
+
private acquire;
|
|
14
|
+
private release;
|
|
15
|
+
request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
16
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { OnyxApiError } from "./errors.js";
|
|
2
|
+
import { getRequestSignal } from "./request-context.js";
|
|
3
|
+
function addQuery(url, query) {
|
|
4
|
+
const serialize = (value) => typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
5
|
+
for (const [key, value] of Object.entries(query)) {
|
|
6
|
+
if (value === undefined || value === null || value === "")
|
|
7
|
+
continue;
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
for (const item of value)
|
|
10
|
+
url.searchParams.append(key, serialize(item));
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
url.searchParams.set(key, serialize(value));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class OnyxClient {
|
|
18
|
+
config;
|
|
19
|
+
fetchImpl;
|
|
20
|
+
activeRequests = 0;
|
|
21
|
+
waiters = [];
|
|
22
|
+
constructor(config, fetchImpl = fetch) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.fetchImpl = fetchImpl;
|
|
25
|
+
}
|
|
26
|
+
async acquire(signal) {
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
if (this.activeRequests < (this.config.maxConcurrency ?? 8)) {
|
|
29
|
+
this.activeRequests += 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
const resume = () => {
|
|
34
|
+
signal?.removeEventListener("abort", abort);
|
|
35
|
+
this.activeRequests += 1;
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const abort = () => {
|
|
39
|
+
const index = this.waiters.indexOf(resume);
|
|
40
|
+
if (index >= 0)
|
|
41
|
+
this.waiters.splice(index, 1);
|
|
42
|
+
reject(new DOMException("The operation was aborted", "AbortError"));
|
|
43
|
+
};
|
|
44
|
+
this.waiters.push(resume);
|
|
45
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
release() {
|
|
49
|
+
this.activeRequests -= 1;
|
|
50
|
+
this.waiters.shift()?.();
|
|
51
|
+
}
|
|
52
|
+
async request(path, options = {}) {
|
|
53
|
+
const baseUrl = new URL(`${this.config.apiUrl}/`);
|
|
54
|
+
const url = new URL(path.startsWith("/") ? path.slice(1) : path, baseUrl);
|
|
55
|
+
const apiPrefix = baseUrl.pathname.endsWith("/")
|
|
56
|
+
? baseUrl.pathname
|
|
57
|
+
: `${baseUrl.pathname}/`;
|
|
58
|
+
if (url.origin !== baseUrl.origin ||
|
|
59
|
+
!`${url.pathname}/`.startsWith(apiPrefix)) {
|
|
60
|
+
throw new Error("Onyx API path must stay within the configured API base URL");
|
|
61
|
+
}
|
|
62
|
+
if (options.query)
|
|
63
|
+
addQuery(url, options.query);
|
|
64
|
+
const callerSignal = getRequestSignal();
|
|
65
|
+
callerSignal?.throwIfAborted();
|
|
66
|
+
await this.acquire(callerSignal);
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
|
69
|
+
let timeout;
|
|
70
|
+
const headers = {
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
Authorization: `Bearer ${this.config.apiToken}`,
|
|
73
|
+
};
|
|
74
|
+
let body;
|
|
75
|
+
if (options.body !== undefined) {
|
|
76
|
+
headers["Content-Type"] = "application/json";
|
|
77
|
+
body = JSON.stringify(options.body);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
callerSignal?.throwIfAborted();
|
|
81
|
+
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
82
|
+
timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
83
|
+
const requestInit = {
|
|
84
|
+
method: options.method ?? "GET",
|
|
85
|
+
headers,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
...(body === undefined ? {} : { body }),
|
|
88
|
+
};
|
|
89
|
+
const response = await this.fetchImpl(url, requestInit);
|
|
90
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
91
|
+
if (Number.isFinite(declaredLength) &&
|
|
92
|
+
declaredLength > this.config.maxResponseBytes) {
|
|
93
|
+
controller.abort();
|
|
94
|
+
throw new OnyxApiError(`Onyx response exceeded ${this.config.maxResponseBytes} bytes`, response.status);
|
|
95
|
+
}
|
|
96
|
+
const chunks = [];
|
|
97
|
+
let receivedBytes = 0;
|
|
98
|
+
if (response.body) {
|
|
99
|
+
const reader = response.body.getReader();
|
|
100
|
+
while (true) {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done)
|
|
103
|
+
break;
|
|
104
|
+
receivedBytes += value.byteLength;
|
|
105
|
+
if (receivedBytes > this.config.maxResponseBytes) {
|
|
106
|
+
await reader.cancel();
|
|
107
|
+
controller.abort();
|
|
108
|
+
throw new OnyxApiError(`Onyx response exceeded ${this.config.maxResponseBytes} bytes`, response.status);
|
|
109
|
+
}
|
|
110
|
+
chunks.push(value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const bytes = new Uint8Array(receivedBytes);
|
|
114
|
+
let offset = 0;
|
|
115
|
+
for (const chunk of chunks) {
|
|
116
|
+
bytes.set(chunk, offset);
|
|
117
|
+
offset += chunk.byteLength;
|
|
118
|
+
}
|
|
119
|
+
const text = new TextDecoder().decode(bytes);
|
|
120
|
+
let data = null;
|
|
121
|
+
if (text) {
|
|
122
|
+
try {
|
|
123
|
+
data = JSON.parse(text);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
data = text;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const detail = typeof data === "object" && data !== null && "detail" in data
|
|
131
|
+
? String(data.detail)
|
|
132
|
+
: response.statusText;
|
|
133
|
+
throw new OnyxApiError(`Onyx request failed: ${detail}`, response.status, data);
|
|
134
|
+
}
|
|
135
|
+
return data;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof OnyxApiError)
|
|
139
|
+
throw error;
|
|
140
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
141
|
+
if (callerSignal?.aborted) {
|
|
142
|
+
throw new Error("Onyx request was cancelled", { cause: error });
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Onyx request timed out after ${this.config.timeoutMs}ms`, { cause: error });
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
if (timeout !== undefined)
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
152
|
+
this.release();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiToken: string;
|
|
4
|
+
defaultPersonaId: number;
|
|
5
|
+
enableWrite: boolean;
|
|
6
|
+
enableAdmin: boolean;
|
|
7
|
+
enableDestructive: boolean;
|
|
8
|
+
enableRawApi: boolean;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
maxResponseBytes: number;
|
|
11
|
+
maxConcurrency?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const booleanValue = z
|
|
3
|
+
.enum(["true", "false", "1", "0"])
|
|
4
|
+
.optional()
|
|
5
|
+
.transform((value) => value === "true" || value === "1");
|
|
6
|
+
const envSchema = z.object({
|
|
7
|
+
ONYX_API_URL: z
|
|
8
|
+
.string()
|
|
9
|
+
.url()
|
|
10
|
+
.refine((value) => {
|
|
11
|
+
const url = new URL(value);
|
|
12
|
+
return (url.protocol === "https:" ||
|
|
13
|
+
(url.protocol === "http:" &&
|
|
14
|
+
["localhost", "127.0.0.1", "::1"].includes(url.hostname)));
|
|
15
|
+
}, "ONYX_API_URL must use HTTPS unless it points to a loopback host"),
|
|
16
|
+
ONYX_API_TOKEN: z.string().min(1),
|
|
17
|
+
ONYX_DEFAULT_PERSONA_ID: z.coerce.number().int().nonnegative().default(0),
|
|
18
|
+
ONYX_MCP_ENABLE_WRITE: booleanValue,
|
|
19
|
+
ONYX_MCP_ENABLE_ADMIN: booleanValue,
|
|
20
|
+
ONYX_MCP_ENABLE_DESTRUCTIVE: booleanValue,
|
|
21
|
+
ONYX_MCP_ENABLE_RAW_API: booleanValue,
|
|
22
|
+
ONYX_MCP_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
|
|
23
|
+
ONYX_MCP_MAX_RESPONSE_BYTES: z.coerce
|
|
24
|
+
.number()
|
|
25
|
+
.int()
|
|
26
|
+
.positive()
|
|
27
|
+
.default(1_000_000),
|
|
28
|
+
ONYX_MCP_MAX_CONCURRENCY: z.coerce
|
|
29
|
+
.number()
|
|
30
|
+
.int()
|
|
31
|
+
.positive()
|
|
32
|
+
.max(100)
|
|
33
|
+
.default(8),
|
|
34
|
+
});
|
|
35
|
+
export function loadConfig(env = process.env) {
|
|
36
|
+
const parsed = envSchema.parse(env);
|
|
37
|
+
return {
|
|
38
|
+
apiUrl: parsed.ONYX_API_URL.replace(/\/$/, ""),
|
|
39
|
+
apiToken: parsed.ONYX_API_TOKEN,
|
|
40
|
+
defaultPersonaId: parsed.ONYX_DEFAULT_PERSONA_ID,
|
|
41
|
+
enableWrite: parsed.ONYX_MCP_ENABLE_WRITE,
|
|
42
|
+
enableAdmin: parsed.ONYX_MCP_ENABLE_ADMIN,
|
|
43
|
+
enableDestructive: parsed.ONYX_MCP_ENABLE_DESTRUCTIVE,
|
|
44
|
+
enableRawApi: parsed.ONYX_MCP_ENABLE_RAW_API,
|
|
45
|
+
timeoutMs: parsed.ONYX_MCP_TIMEOUT_MS,
|
|
46
|
+
maxResponseBytes: parsed.ONYX_MCP_MAX_RESPONSE_BYTES,
|
|
47
|
+
maxConcurrency: parsed.ONYX_MCP_MAX_CONCURRENCY,
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class OnyxApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
details;
|
|
4
|
+
constructor(message, status, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "OnyxApiError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function safeErrorMessage(error) {
|
|
12
|
+
if (error instanceof OnyxApiError) {
|
|
13
|
+
return `${error.message} (HTTP ${error.status})`;
|
|
14
|
+
}
|
|
15
|
+
return error instanceof Error ? error.message : "Unknown error";
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { z, ZodRawShape } from "zod";
|
|
4
|
+
import type { Config } from "./config.js";
|
|
5
|
+
import type { OnyxClient } from "./client.js";
|
|
6
|
+
export interface ToolContext {
|
|
7
|
+
server: McpServer;
|
|
8
|
+
client: OnyxClient;
|
|
9
|
+
config: Config;
|
|
10
|
+
}
|
|
11
|
+
export declare function addTool<T extends ZodRawShape>(server: McpServer, name: string, description: string, schema: T, handler: (args: z.infer<z.ZodObject<T>>) => Promise<CallToolResult>, annotations?: ToolAnnotations): void;
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { runWithRequestSignal } from "./request-context.js";
|
|
2
|
+
export function addTool(server, name, description, schema, handler, annotations = {
|
|
3
|
+
readOnlyHint: false,
|
|
4
|
+
destructiveHint: false,
|
|
5
|
+
idempotentHint: false,
|
|
6
|
+
openWorldHint: false,
|
|
7
|
+
}) {
|
|
8
|
+
const completeAnnotations = {
|
|
9
|
+
readOnlyHint: false,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: false,
|
|
12
|
+
openWorldHint: false,
|
|
13
|
+
...annotations,
|
|
14
|
+
};
|
|
15
|
+
server.registerTool(name, {
|
|
16
|
+
description,
|
|
17
|
+
inputSchema: schema,
|
|
18
|
+
annotations: completeAnnotations,
|
|
19
|
+
}, ((args, extra) => runWithRequestSignal(extra.signal, () => handler(args))));
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const signalContext = new AsyncLocalStorage();
|
|
3
|
+
export function runWithRequestSignal(signal, action) {
|
|
4
|
+
return signalContext.run(signal, action);
|
|
5
|
+
}
|
|
6
|
+
export function getRequestSignal() {
|
|
7
|
+
return signalContext.getStore();
|
|
8
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const filtersSchema: z.ZodObject<{
|
|
3
|
+
document_sets: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
4
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
5
|
+
time_cutoff: z.ZodOptional<z.ZodString>;
|
|
6
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
7
|
+
tag_key: z.ZodString;
|
|
8
|
+
tag_value: z.ZodString;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
tag_key: string;
|
|
11
|
+
tag_value: string;
|
|
12
|
+
}, {
|
|
13
|
+
tag_key: string;
|
|
14
|
+
tag_value: string;
|
|
15
|
+
}>, "many">>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
document_sets?: string[] | undefined;
|
|
18
|
+
sources?: string[] | undefined;
|
|
19
|
+
time_cutoff?: string | undefined;
|
|
20
|
+
tags?: {
|
|
21
|
+
tag_key: string;
|
|
22
|
+
tag_value: string;
|
|
23
|
+
}[] | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
document_sets?: string[] | undefined;
|
|
26
|
+
sources?: string[] | undefined;
|
|
27
|
+
time_cutoff?: string | undefined;
|
|
28
|
+
tags?: {
|
|
29
|
+
tag_key: string;
|
|
30
|
+
tag_value: string;
|
|
31
|
+
}[] | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
export declare const httpUrlSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
34
|
+
export declare const paginationSchema: {
|
|
35
|
+
offset: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
};
|
|
38
|
+
export declare const rawPathSchema: z.ZodEffects<z.ZodString, string, string>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const filtersSchema = z.object({
|
|
3
|
+
document_sets: z.array(z.string()).optional(),
|
|
4
|
+
sources: z.array(z.string()).optional(),
|
|
5
|
+
time_cutoff: z.string().datetime().optional(),
|
|
6
|
+
tags: z
|
|
7
|
+
.array(z.object({ tag_key: z.string().min(1), tag_value: z.string() }))
|
|
8
|
+
.optional(),
|
|
9
|
+
});
|
|
10
|
+
export const httpUrlSchema = z
|
|
11
|
+
.string()
|
|
12
|
+
.url()
|
|
13
|
+
.refine((value) => {
|
|
14
|
+
const protocol = new URL(value).protocol;
|
|
15
|
+
return protocol === "http:" || protocol === "https:";
|
|
16
|
+
}, "URL must use HTTP or HTTPS");
|
|
17
|
+
export const paginationSchema = {
|
|
18
|
+
offset: z.number().int().nonnegative().optional(),
|
|
19
|
+
limit: z.number().int().positive().max(1000).optional(),
|
|
20
|
+
};
|
|
21
|
+
export const rawPathSchema = z
|
|
22
|
+
.string()
|
|
23
|
+
.startsWith("/")
|
|
24
|
+
.regex(/^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$/, "Path contains unsafe characters")
|
|
25
|
+
.refine((path) => !path.includes("//") && !path.split("/").includes(".."), "Path must be relative to the Onyx API");
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Config } from "./config.js";
|
|
3
|
+
export { OnyxClient } from "./client.js";
|
|
4
|
+
export { loadConfig, type Config } from "./config.js";
|
|
5
|
+
export declare function createServer(config: Config, fetchImpl?: typeof fetch): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { OnyxClient } from "./client.js";
|
|
3
|
+
import { registerAdminTools } from "./tools/admin.js";
|
|
4
|
+
import { registerChatTools } from "./tools/chat.js";
|
|
5
|
+
import { registerProjectTools } from "./tools/projects.js";
|
|
6
|
+
import { registerRawApiTool } from "./tools/raw.js";
|
|
7
|
+
import { registerSearchTools } from "./tools/search.js";
|
|
8
|
+
import { registerSystemTools } from "./tools/system.js";
|
|
9
|
+
import { VERSION } from "./version.js";
|
|
10
|
+
export { OnyxClient } from "./client.js";
|
|
11
|
+
export { loadConfig } from "./config.js";
|
|
12
|
+
export function createServer(config, fetchImpl = fetch) {
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: "onyx-mcp",
|
|
15
|
+
title: "Onyx MCP",
|
|
16
|
+
description: "Secure MCP access to Onyx search, chat, projects, and administration.",
|
|
17
|
+
websiteUrl: "https://github.com/ByWaleed/onyx-mcp",
|
|
18
|
+
version: VERSION,
|
|
19
|
+
});
|
|
20
|
+
const client = new OnyxClient(config, fetchImpl);
|
|
21
|
+
const context = { server, client, config };
|
|
22
|
+
registerSystemTools(context);
|
|
23
|
+
registerSearchTools(context);
|
|
24
|
+
registerChatTools(context);
|
|
25
|
+
registerProjectTools(context);
|
|
26
|
+
registerAdminTools(context);
|
|
27
|
+
registerRawApiTool(context);
|
|
28
|
+
return server;
|
|
29
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
export declare function success(data: unknown): CallToolResult;
|
|
3
|
+
export declare function failure(error: unknown): CallToolResult;
|
|
4
|
+
export declare function runTool(action: () => Promise<unknown>): Promise<CallToolResult>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { safeErrorMessage } from "./errors.js";
|
|
2
|
+
export function success(data) {
|
|
3
|
+
return {
|
|
4
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
5
|
+
structuredContent: { result: data },
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function failure(error) {
|
|
9
|
+
return {
|
|
10
|
+
isError: true,
|
|
11
|
+
content: [{ type: "text", text: safeErrorMessage(error) }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function runTool(action) {
|
|
15
|
+
try {
|
|
16
|
+
return success(await action());
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return failure(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addTool } from "../register.js";
|
|
3
|
+
import { runTool } from "../tool-helpers.js";
|
|
4
|
+
export function registerAdminTools({ server, client, config, }) {
|
|
5
|
+
if (!config.enableAdmin)
|
|
6
|
+
return;
|
|
7
|
+
addTool(server, "onyx_admin_list_connectors", "List configured Onyx connectors.", {}, () => runTool(() => client.request("/manage/admin/connector")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
8
|
+
addTool(server, "onyx_admin_connector_status", "Get detailed indexing status for Onyx connectors.", {}, () => runTool(() => client.request("/manage/admin/connector/status")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
9
|
+
addTool(server, "onyx_admin_list_credentials", "List credential metadata. Secret values are controlled and masked by Onyx.", {}, () => runTool(() => client.request("/manage/admin/credential")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
10
|
+
addTool(server, "onyx_admin_list_users", "List users managed by this Onyx deployment.", {}, () => runTool(() => client.request("/manage/users")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
11
|
+
addTool(server, "onyx_admin_list_agents", "List agents with administrative metadata.", {}, () => runTool(() => client.request("/admin/agents")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
12
|
+
addTool(server, "onyx_admin_list_ingested_documents", "List documents added through the direct ingestion API.", {}, () => runTool(() => client.request("/onyx-api/ingestion")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
13
|
+
if (!config.enableWrite)
|
|
14
|
+
return;
|
|
15
|
+
addTool(server, "onyx_admin_ingest_document", "Create or update a document through Onyx's direct ingestion API. This changes the search index.", {
|
|
16
|
+
document: z.object({
|
|
17
|
+
id: z.string().optional(),
|
|
18
|
+
semantic_identifier: z.string().min(1),
|
|
19
|
+
sections: z
|
|
20
|
+
.array(z.object({
|
|
21
|
+
type: z.literal("text"),
|
|
22
|
+
text: z.string(),
|
|
23
|
+
link: z.string().url().nullable().optional(),
|
|
24
|
+
heading: z.string().nullable().optional(),
|
|
25
|
+
}))
|
|
26
|
+
.min(1),
|
|
27
|
+
source: z.string().optional(),
|
|
28
|
+
metadata: z.record(z.union([z.string(), z.array(z.string())])),
|
|
29
|
+
title: z.string().nullable().optional(),
|
|
30
|
+
doc_updated_at: z.string().datetime().optional(),
|
|
31
|
+
doc_created_at: z.string().datetime().optional(),
|
|
32
|
+
}),
|
|
33
|
+
cc_pair_id: z.number().int().positive().optional(),
|
|
34
|
+
}, ({ document, cc_pair_id }) => runTool(() => client.request("/onyx-api/ingestion", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
body: { document, cc_pair_id },
|
|
37
|
+
})), { readOnlyHint: false, destructiveHint: false, openWorldHint: false });
|
|
38
|
+
addTool(server, "onyx_admin_run_connector", "Start one connector indexing run. This can consume significant resources.", {
|
|
39
|
+
connector_id: z.number().int().positive(),
|
|
40
|
+
credential_ids: z.array(z.number().int().positive()).optional(),
|
|
41
|
+
from_beginning: z.boolean().optional(),
|
|
42
|
+
}, (body) => runTool(() => client.request("/manage/admin/connector/run-once", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body,
|
|
45
|
+
})), { readOnlyHint: false, destructiveHint: false, openWorldHint: false });
|
|
46
|
+
if (!config.enableDestructive)
|
|
47
|
+
return;
|
|
48
|
+
addTool(server, "onyx_admin_delete_ingested_document", "Delete a document created through the direct ingestion API. This action is destructive.", { document_id: z.string().min(1), confirm: z.literal(true) }, ({ document_id }) => runTool(() => client.request(`/onyx-api/ingestion/${encodeURIComponent(document_id)}`, {
|
|
49
|
+
method: "DELETE",
|
|
50
|
+
})), {
|
|
51
|
+
readOnlyHint: false,
|
|
52
|
+
destructiveHint: true,
|
|
53
|
+
idempotentHint: true,
|
|
54
|
+
openWorldHint: false,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addTool } from "../register.js";
|
|
3
|
+
import { filtersSchema } from "../schemas.js";
|
|
4
|
+
import { runTool } from "../tool-helpers.js";
|
|
5
|
+
export function registerChatTools({ server, client, config, }) {
|
|
6
|
+
addTool(server, "onyx_list_chats", "List chat sessions owned by the authenticated user.", {
|
|
7
|
+
page_size: z.number().int().positive().max(100).optional(),
|
|
8
|
+
project_id: z.number().int().positive().optional(),
|
|
9
|
+
only_non_project_chats: z.boolean().optional(),
|
|
10
|
+
include_failed_chats: z.boolean().optional(),
|
|
11
|
+
before: z.string().datetime().optional(),
|
|
12
|
+
}, (args) => runTool(() => client.request("/chat/get-user-chat-sessions", { query: args })), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
13
|
+
addTool(server, "onyx_get_chat", "Get a chat session and its message history.", { chat_session_id: z.string().uuid() }, ({ chat_session_id }) => runTool(() => client.request(`/chat/get-chat-session/${chat_session_id}`)), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
14
|
+
addTool(server, "onyx_search_chats", "Search the authenticated user's chat history.", {
|
|
15
|
+
query: z.string().min(1),
|
|
16
|
+
page: z.number().int().positive().optional(),
|
|
17
|
+
page_size: z.number().int().positive().max(100).optional(),
|
|
18
|
+
}, (args) => runTool(() => client.request("/chat/search", { query: args })), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
19
|
+
if (!config.enableWrite)
|
|
20
|
+
return;
|
|
21
|
+
addTool(server, "onyx_create_chat", "Create a persistent Onyx chat session.", {
|
|
22
|
+
persona_id: z.number().int().nonnegative().optional(),
|
|
23
|
+
description: z.string().max(500).optional(),
|
|
24
|
+
project_id: z.number().int().positive().optional(),
|
|
25
|
+
incognito: z.boolean().optional(),
|
|
26
|
+
}, (args) => runTool(() => client.request("/chat/create-chat-session", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: {
|
|
29
|
+
...args,
|
|
30
|
+
persona_id: args.persona_id ?? config.defaultPersonaId,
|
|
31
|
+
},
|
|
32
|
+
})), { readOnlyHint: false, destructiveHint: false, openWorldHint: false });
|
|
33
|
+
addTool(server, "onyx_chat", "Ask an Onyx agent a question. Creates a persistent session when chat_session_id is omitted. May invoke search, tools, and an LLM.", {
|
|
34
|
+
message: z.string().min(1),
|
|
35
|
+
chat_session_id: z.string().uuid().optional(),
|
|
36
|
+
persona_id: z.number().int().nonnegative().optional(),
|
|
37
|
+
filters: filtersSchema.optional(),
|
|
38
|
+
deep_research: z.boolean().optional(),
|
|
39
|
+
allowed_tool_ids: z.array(z.number().int().positive()).optional(),
|
|
40
|
+
forced_tool_id: z.number().int().positive().optional(),
|
|
41
|
+
include_citations: z.boolean().optional(),
|
|
42
|
+
}, ({ message, chat_session_id, persona_id, filters, ...options }) => runTool(() => client.request("/chat/send-chat-message", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
body: {
|
|
45
|
+
message,
|
|
46
|
+
...(chat_session_id
|
|
47
|
+
? { chat_session_id }
|
|
48
|
+
: {
|
|
49
|
+
chat_session_info: {
|
|
50
|
+
persona_id: persona_id ?? config.defaultPersonaId,
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
internal_search_filters: filters,
|
|
54
|
+
stream: false,
|
|
55
|
+
...options,
|
|
56
|
+
},
|
|
57
|
+
})), { readOnlyHint: false, destructiveHint: false, openWorldHint: true });
|
|
58
|
+
addTool(server, "onyx_stop_chat", "Stop active generation for a chat session.", { chat_session_id: z.string().uuid() }, ({ chat_session_id }) => runTool(() => client.request(`/chat/stop-chat-session/${chat_session_id}`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
})), {
|
|
61
|
+
readOnlyHint: false,
|
|
62
|
+
destructiveHint: false,
|
|
63
|
+
idempotentHint: true,
|
|
64
|
+
openWorldHint: false,
|
|
65
|
+
});
|
|
66
|
+
addTool(server, "onyx_submit_feedback", "Add or update feedback for an Onyx chat message.", {
|
|
67
|
+
chat_message_id: z.number().int().positive(),
|
|
68
|
+
is_positive: z.boolean().nullable().optional(),
|
|
69
|
+
feedback_text: z.string().max(5000).optional(),
|
|
70
|
+
predefined_feedback: z.string().max(500).optional(),
|
|
71
|
+
}, (body) => runTool(() => {
|
|
72
|
+
if (body.is_positive === undefined && !body.feedback_text) {
|
|
73
|
+
throw new Error("is_positive or feedback_text is required");
|
|
74
|
+
}
|
|
75
|
+
return client.request("/chat/create-chat-message-feedback", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
body,
|
|
78
|
+
});
|
|
79
|
+
}), {
|
|
80
|
+
readOnlyHint: false,
|
|
81
|
+
destructiveHint: false,
|
|
82
|
+
idempotentHint: true,
|
|
83
|
+
openWorldHint: false,
|
|
84
|
+
});
|
|
85
|
+
if (!config.enableDestructive)
|
|
86
|
+
return;
|
|
87
|
+
addTool(server, "onyx_delete_chat", "Delete one Onyx chat session according to the deployment's retention policy.", {
|
|
88
|
+
chat_session_id: z.string().uuid(),
|
|
89
|
+
confirm: z.literal(true).describe("Must be true to confirm deletion."),
|
|
90
|
+
}, ({ chat_session_id }) => runTool(() => client.request(`/chat/delete-chat-session/${chat_session_id}`, {
|
|
91
|
+
method: "DELETE",
|
|
92
|
+
})), {
|
|
93
|
+
readOnlyHint: false,
|
|
94
|
+
destructiveHint: true,
|
|
95
|
+
idempotentHint: true,
|
|
96
|
+
openWorldHint: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addTool } from "../register.js";
|
|
3
|
+
import { runTool } from "../tool-helpers.js";
|
|
4
|
+
export function registerProjectTools({ server, client, config, }) {
|
|
5
|
+
addTool(server, "onyx_list_projects", "List Onyx projects.", {}, () => runTool(() => client.request("/user/projects")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
6
|
+
addTool(server, "onyx_get_project", "Get an Onyx project and its details.", { project_id: z.number().int().positive() }, ({ project_id }) => runTool(() => client.request(`/user/projects/${project_id}/details`)), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
7
|
+
addTool(server, "onyx_list_project_files", "List files linked to an Onyx project.", { project_id: z.number().int().positive() }, ({ project_id }) => runTool(() => client.request(`/user/projects/files/${project_id}`)), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
8
|
+
addTool(server, "onyx_get_file", "Get metadata and processing status for an Onyx user file.", { file_id: z.string().uuid() }, ({ file_id }) => runTool(() => client.request(`/user/projects/file/${encodeURIComponent(file_id)}`)), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
9
|
+
if (!config.enableWrite)
|
|
10
|
+
return;
|
|
11
|
+
addTool(server, "onyx_create_project", "Create an Onyx project.", { name: z.string().min(1).max(255) }, ({ name }) => runTool(() => client.request("/user/projects/create", {
|
|
12
|
+
method: "POST",
|
|
13
|
+
query: { name },
|
|
14
|
+
})), { readOnlyHint: false, destructiveHint: false, openWorldHint: false });
|
|
15
|
+
addTool(server, "onyx_update_project", "Update an Onyx project's name or description.", {
|
|
16
|
+
project_id: z.number().int().positive(),
|
|
17
|
+
name: z.string().min(1).max(255).optional(),
|
|
18
|
+
description: z.string().max(255).optional(),
|
|
19
|
+
}, ({ project_id, ...body }) => runTool(() => client.request(`/user/projects/${project_id}`, {
|
|
20
|
+
method: "PATCH",
|
|
21
|
+
body,
|
|
22
|
+
})), {
|
|
23
|
+
readOnlyHint: false,
|
|
24
|
+
destructiveHint: false,
|
|
25
|
+
idempotentHint: true,
|
|
26
|
+
openWorldHint: false,
|
|
27
|
+
});
|
|
28
|
+
addTool(server, "onyx_set_project_instructions", "Set the instructions used by an Onyx project.", {
|
|
29
|
+
project_id: z.number().int().positive(),
|
|
30
|
+
instructions: z.string().max(20_000),
|
|
31
|
+
}, ({ project_id, instructions }) => runTool(() => client.request(`/user/projects/${project_id}/instructions`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: { instructions },
|
|
34
|
+
})), {
|
|
35
|
+
readOnlyHint: false,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: true,
|
|
38
|
+
openWorldHint: false,
|
|
39
|
+
});
|
|
40
|
+
if (!config.enableDestructive)
|
|
41
|
+
return;
|
|
42
|
+
addTool(server, "onyx_delete_project", "Permanently delete an Onyx project. This action is destructive.", {
|
|
43
|
+
project_id: z.number().int().positive(),
|
|
44
|
+
confirm: z.literal(true),
|
|
45
|
+
}, ({ project_id }) => runTool(() => client.request(`/user/projects/${project_id}`, { method: "DELETE" })), {
|
|
46
|
+
readOnlyHint: false,
|
|
47
|
+
destructiveHint: true,
|
|
48
|
+
idempotentHint: true,
|
|
49
|
+
openWorldHint: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addTool } from "../register.js";
|
|
3
|
+
import { rawPathSchema } from "../schemas.js";
|
|
4
|
+
import { runTool } from "../tool-helpers.js";
|
|
5
|
+
export function registerRawApiTool({ server, client, config, }) {
|
|
6
|
+
if (!config.enableRawApi)
|
|
7
|
+
return;
|
|
8
|
+
addTool(server, "onyx_api_request", "Call a deployment-specific Onyx API endpoint. Read requests need raw API access. Mutating requests also need the matching write, admin, and destructive profiles. Prefer a first-class tool when available.", {
|
|
9
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
10
|
+
path: rawPathSchema,
|
|
11
|
+
query: z.record(z.unknown()).optional(),
|
|
12
|
+
body: z.unknown().optional(),
|
|
13
|
+
confirm: z.boolean().optional(),
|
|
14
|
+
}, ({ method, path, query, body, confirm }) => runTool(async () => {
|
|
15
|
+
const mutating = method !== "GET";
|
|
16
|
+
const destructive = method === "DELETE";
|
|
17
|
+
if (!config.enableAdmin) {
|
|
18
|
+
throw new Error("Raw API requests require ONYX_MCP_ENABLE_ADMIN=true");
|
|
19
|
+
}
|
|
20
|
+
if (mutating && !config.enableWrite) {
|
|
21
|
+
throw new Error("Mutating raw API requests require ONYX_MCP_ENABLE_WRITE=true");
|
|
22
|
+
}
|
|
23
|
+
if (destructive && !config.enableDestructive) {
|
|
24
|
+
throw new Error("Destructive raw API requests require ONYX_MCP_ENABLE_DESTRUCTIVE=true");
|
|
25
|
+
}
|
|
26
|
+
if (destructive && confirm !== true) {
|
|
27
|
+
throw new Error("Destructive raw API requests require confirm=true");
|
|
28
|
+
}
|
|
29
|
+
return client.request(path, {
|
|
30
|
+
method,
|
|
31
|
+
...(query === undefined ? {} : { query }),
|
|
32
|
+
...(body === undefined ? {} : { body }),
|
|
33
|
+
});
|
|
34
|
+
}), { readOnlyHint: false, destructiveHint: true, openWorldHint: true });
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addTool } from "../register.js";
|
|
3
|
+
import { filtersSchema, httpUrlSchema } from "../schemas.js";
|
|
4
|
+
import { runTool } from "../tool-helpers.js";
|
|
5
|
+
export function registerSearchTools({ server, client }) {
|
|
6
|
+
addTool(server, "onyx_search", "Search indexed Onyx knowledge with access controls enforced by Onyx.", {
|
|
7
|
+
query: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(2048)
|
|
11
|
+
.describe("Search query, up to 2,048 characters."),
|
|
12
|
+
filters: filtersSchema.optional(),
|
|
13
|
+
persona_id: z.number().int().nonnegative().optional(),
|
|
14
|
+
provider: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Model provider; model is also required."),
|
|
18
|
+
model: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Model name; provider is also required."),
|
|
22
|
+
skip_query_expansion: z.boolean().optional(),
|
|
23
|
+
}, (args) => runTool(() => (() => {
|
|
24
|
+
if ((args.provider === undefined) !== (args.model === undefined)) {
|
|
25
|
+
throw new Error("provider and model must be supplied together");
|
|
26
|
+
}
|
|
27
|
+
const { query, filters, ...options } = args;
|
|
28
|
+
return client.request("/search", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: { query, ...filters, ...options },
|
|
31
|
+
});
|
|
32
|
+
})()), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
33
|
+
addTool(server, "onyx_search_tags", "Find valid indexed tags for search filtering.", {
|
|
34
|
+
match_pattern: z.string().optional(),
|
|
35
|
+
sources: z.array(z.string()).optional(),
|
|
36
|
+
limit: z.number().int().positive().max(1000).optional(),
|
|
37
|
+
}, (args) => runTool(() => client.request("/query/valid-tags", { query: args })), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
38
|
+
addTool(server, "onyx_web_search", "Search the web through the provider configured in Onyx. This may incur provider costs.", {
|
|
39
|
+
queries: z.array(z.string().min(1)).min(1).max(20),
|
|
40
|
+
max_results: z.number().int().positive().max(100).optional(),
|
|
41
|
+
}, (body) => runTool(() => client.request("/web-search/search-lite", {
|
|
42
|
+
method: "POST",
|
|
43
|
+
body,
|
|
44
|
+
})), { readOnlyHint: true, openWorldHint: true });
|
|
45
|
+
addTool(server, "onyx_open_urls", "Fetch and extract web pages through Onyx.", { urls: z.array(httpUrlSchema).min(1).max(20) }, ({ urls }) => runTool(() => client.request("/web-search/open-urls", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: { urls },
|
|
48
|
+
})), { readOnlyHint: true, openWorldHint: true });
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { addTool } from "../register.js";
|
|
3
|
+
import { runTool } from "../tool-helpers.js";
|
|
4
|
+
export function registerSystemTools({ server, client, config, }) {
|
|
5
|
+
addTool(server, "onyx_health", "Check whether the Onyx API is healthy.", {}, () => runTool(() => client.request("/health")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
6
|
+
addTool(server, "onyx_version", "Get the Onyx server version.", {}, () => runTool(() => client.request("/version")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
7
|
+
addTool(server, "onyx_whoami", "Get the authenticated Onyx user.", {}, () => runTool(() => client.request("/me")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
8
|
+
addTool(server, "onyx_permissions", "Get permissions granted to the authenticated Onyx user.", {}, () => runTool(() => client.request("/me/permissions")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
9
|
+
addTool(server, "onyx_capabilities", "Show this MCP server's enabled safety profiles and default assistant.", {}, () => runTool(() => Promise.resolve({
|
|
10
|
+
write: config.enableWrite,
|
|
11
|
+
admin: config.enableAdmin,
|
|
12
|
+
destructive: config.enableDestructive,
|
|
13
|
+
rawApi: config.enableRawApi,
|
|
14
|
+
defaultPersonaId: config.defaultPersonaId,
|
|
15
|
+
})), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
16
|
+
addTool(server, "onyx_list_agents", "List Onyx agents available to the authenticated user.", {
|
|
17
|
+
page_num: z.number().int().nonnegative().optional(),
|
|
18
|
+
page_size: z.number().int().positive().max(100).optional(),
|
|
19
|
+
include_deleted: z.boolean().optional(),
|
|
20
|
+
get_editable: z.boolean().optional(),
|
|
21
|
+
include_default: z.boolean().optional(),
|
|
22
|
+
}, (args) => runTool(() => client.request("/agents", { query: args })), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
23
|
+
addTool(server, "onyx_get_agent", "Get one Onyx agent by ID.", { persona_id: z.number().int().nonnegative() }, ({ persona_id }) => runTool(() => client.request(`/persona/${persona_id}`)), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
24
|
+
addTool(server, "onyx_list_tools", "List tools configured in Onyx.", {}, () => runTool(() => client.request("/tool")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
25
|
+
addTool(server, "onyx_list_document_sets", "List document sets visible to the authenticated user.", { editable_only: z.boolean().optional() }, ({ editable_only }) => runTool(() => client.request("/manage/document-set", {
|
|
26
|
+
query: { get_editable: editable_only },
|
|
27
|
+
})), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
28
|
+
addTool(server, "onyx_list_indexed_sources", "List source types indexed by Onyx.", {}, () => runTool(() => client.request("/manage/indexed-sources")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
29
|
+
addTool(server, "onyx_connector_status", "Get status summaries for Onyx connectors.", {}, () => runTool(() => client.request("/manage/connector-status")), { readOnlyHint: true, idempotentHint: true, openWorldHint: false });
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.1.0";
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bywaleed/onyx-mcp",
|
|
3
|
+
"mcpName": "io.github.bywaleed/onyx-mcp",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "A comprehensive, secure Model Context Protocol server for Onyx.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/server.js",
|
|
8
|
+
"types": "dist/server.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/server.d.ts",
|
|
12
|
+
"import": "./dist/server.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"onyx-mcp": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"SECURITY.md",
|
|
23
|
+
"server.json"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"version:sync": "node scripts/sync-version.mjs",
|
|
28
|
+
"build": "npm run version:sync && npm run clean && tsc -p tsconfig.json",
|
|
29
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
30
|
+
"dev": "tsx src/cli.ts",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"format:check": "prettier --check .",
|
|
33
|
+
"lint": "eslint .",
|
|
34
|
+
"start": "node dist/cli.js",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"verify": "npm run version:sync && npm run format:check && npm run lint && npm run check && npm test && npm run build",
|
|
39
|
+
"prepack": "npm run verify"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"onyx",
|
|
46
|
+
"danswer",
|
|
47
|
+
"mcp",
|
|
48
|
+
"model-context-protocol",
|
|
49
|
+
"enterprise-search",
|
|
50
|
+
"rag"
|
|
51
|
+
],
|
|
52
|
+
"author": "Waleed Islam",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"packageManager": "npm@11.6.2",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"provenance": true
|
|
58
|
+
},
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/ByWaleed/onyx-mcp.git"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/ByWaleed/onyx-mcp/issues"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://github.com/ByWaleed/onyx-mcp#readme",
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
69
|
+
"zod": "^3.25.76"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@eslint/js": "^10.0.1",
|
|
73
|
+
"@types/node": "^22.15.3",
|
|
74
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
75
|
+
"eslint": "^10.0.0",
|
|
76
|
+
"globals": "^16.3.0",
|
|
77
|
+
"prettier": "^3.6.2",
|
|
78
|
+
"tsx": "^4.19.4",
|
|
79
|
+
"typescript": "^5.8.3",
|
|
80
|
+
"typescript-eslint": "^8.68.0",
|
|
81
|
+
"vitest": "^3.1.2"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.bywaleed/onyx-mcp",
|
|
4
|
+
"title": "Onyx MCP",
|
|
5
|
+
"description": "Secure MCP access to Onyx search, chat, projects, and administration.",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"websiteUrl": "https://github.com/ByWaleed/onyx-mcp",
|
|
8
|
+
"repository": {
|
|
9
|
+
"url": "https://github.com/ByWaleed/onyx-mcp",
|
|
10
|
+
"source": "github",
|
|
11
|
+
"id": "1351600077"
|
|
12
|
+
},
|
|
13
|
+
"packages": [
|
|
14
|
+
{
|
|
15
|
+
"registryType": "npm",
|
|
16
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
17
|
+
"identifier": "@bywaleed/onyx-mcp",
|
|
18
|
+
"version": "0.1.0",
|
|
19
|
+
"runtimeHint": "npx",
|
|
20
|
+
"transport": {
|
|
21
|
+
"type": "stdio"
|
|
22
|
+
},
|
|
23
|
+
"environmentVariables": [
|
|
24
|
+
{
|
|
25
|
+
"name": "ONYX_API_URL",
|
|
26
|
+
"description": "Onyx API base URL, for example https://onyx.example.com/api",
|
|
27
|
+
"isRequired": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "ONYX_API_TOKEN",
|
|
31
|
+
"description": "Onyx Personal Access Token or API key",
|
|
32
|
+
"isRequired": true,
|
|
33
|
+
"isSecret": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "ONYX_DEFAULT_PERSONA_ID",
|
|
37
|
+
"description": "Default Onyx agent ID for new chats",
|
|
38
|
+
"default": "0"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|