@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 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
+ [![CI](https://github.com/ByWaleed/onyx-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/ByWaleed/onyx-mcp/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ });
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export declare class OnyxApiError extends Error {
2
+ readonly status: number;
3
+ readonly details?: unknown | undefined;
4
+ constructor(message: string, status: number, details?: unknown | undefined);
5
+ }
6
+ export declare function safeErrorMessage(error: unknown): string;
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;
@@ -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,2 @@
1
+ export declare function runWithRequestSignal<T>(signal: AbortSignal, action: () => Promise<T>): Promise<T>;
2
+ export declare function getRequestSignal(): AbortSignal | undefined;
@@ -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>;
@@ -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");
@@ -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,2 @@
1
+ import type { ToolContext } from "../register.js";
2
+ export declare function registerAdminTools({ server, client, config, }: ToolContext): void;
@@ -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,2 @@
1
+ import type { ToolContext } from "../register.js";
2
+ export declare function registerChatTools({ server, client, config, }: ToolContext): void;
@@ -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,2 @@
1
+ import type { ToolContext } from "../register.js";
2
+ export declare function registerProjectTools({ server, client, config, }: ToolContext): void;
@@ -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,2 @@
1
+ import type { ToolContext } from "../register.js";
2
+ export declare function registerRawApiTool({ server, client, config, }: ToolContext): void;
@@ -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,2 @@
1
+ import type { ToolContext } from "../register.js";
2
+ export declare function registerSearchTools({ server, client }: ToolContext): void;
@@ -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,2 @@
1
+ import type { ToolContext } from "../register.js";
2
+ export declare function registerSystemTools({ server, client, config, }: ToolContext): void;
@@ -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";
@@ -0,0 +1,2 @@
1
+ // Generated from package.json by scripts/sync-version.mjs.
2
+ export const VERSION = "0.1.0";
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
+ }