@manfred-kunze-dev/backbone-mcp-server 2.6.0-dev.4

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.
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ import { formatErrorForMcp } from "../errors.js";
3
+ export function register(server, client) {
4
+ // ── chat ────────────────────────────────────────────────────────────────
5
+ server.tool("backbone_chat", "Send a chat completion request through Backbone's OpenAI-compatible AI gateway. Supports multiple providers (OpenAI, Anthropic, Azure, Vertex AI, etc.). Model format: 'provider/model' (e.g. 'openai/gpt-4o', 'anthropic/claude-sonnet-4-5-20250929'). Always non-streaming.", {
6
+ model: z.string().describe("Model identifier in 'provider/model' format"),
7
+ messages: z
8
+ .array(z.object({
9
+ role: z.enum(["system", "user", "assistant", "tool"]),
10
+ content: z.string().nullable(),
11
+ name: z.string().optional(),
12
+ tool_call_id: z.string().optional(),
13
+ }))
14
+ .describe("Chat messages array"),
15
+ temperature: z.number().min(0).max(2).optional().describe("Sampling temperature (0-2)"),
16
+ max_tokens: z.number().optional().describe("Maximum tokens to generate"),
17
+ top_p: z.number().optional().describe("Top-p (nucleus) sampling"),
18
+ frequency_penalty: z.number().optional(),
19
+ presence_penalty: z.number().optional(),
20
+ stop: z.array(z.string()).optional().describe("Stop sequences"),
21
+ response_format: z
22
+ .object({ type: z.enum(["text", "json_object"]) })
23
+ .optional()
24
+ .describe("Response format constraint"),
25
+ tools: z.array(z.unknown()).optional().describe("Tool definitions for function calling"),
26
+ tool_choice: z.unknown().optional().describe("Tool choice: 'auto', 'none', 'required', or object"),
27
+ }, async (params) => {
28
+ try {
29
+ // Build the spec-compliant body, then add non-spec OpenAI-compatible fields
30
+ const body = {
31
+ model: params.model,
32
+ messages: params.messages,
33
+ stream: false,
34
+ ...(params.temperature !== undefined && { temperature: params.temperature }),
35
+ ...(params.max_tokens !== undefined && { max_tokens: params.max_tokens }),
36
+ ...(params.top_p !== undefined && { top_p: params.top_p }),
37
+ ...(params.frequency_penalty !== undefined && {
38
+ frequency_penalty: params.frequency_penalty,
39
+ }),
40
+ ...(params.presence_penalty !== undefined && {
41
+ presence_penalty: params.presence_penalty,
42
+ }),
43
+ ...(params.stop && { stop: params.stop }),
44
+ ...(params.response_format && { response_format: params.response_format }),
45
+ ...(params.tools && { tools: params.tools }),
46
+ ...(params.tool_choice !== undefined && { tool_choice: params.tool_choice }),
47
+ };
48
+ const { data } = await client.POST("/v1/chat/completions", {
49
+ body: body,
50
+ });
51
+ const result = data;
52
+ const parts = [];
53
+ const choices = result.choices;
54
+ if (choices) {
55
+ for (const choice of choices) {
56
+ const msg = choice.message;
57
+ if (msg.content) {
58
+ parts.push({ type: "text", text: msg.content });
59
+ }
60
+ if (msg.tool_calls?.length) {
61
+ parts.push({
62
+ type: "text",
63
+ text: `Tool calls:\n${JSON.stringify(msg.tool_calls, null, 2)}`,
64
+ });
65
+ }
66
+ }
67
+ }
68
+ const usage = result.usage;
69
+ if (usage) {
70
+ parts.push({
71
+ type: "text",
72
+ text: `\n[Usage: ${usage.prompt_tokens} prompt + ${usage.completion_tokens} completion = ${usage.total_tokens} total tokens]`,
73
+ });
74
+ }
75
+ return { content: parts };
76
+ }
77
+ catch (error) {
78
+ return {
79
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
80
+ isError: true,
81
+ };
82
+ }
83
+ });
84
+ // ── list_models ─────────────────────────────────────────────────────────
85
+ server.tool("backbone_list_models", "List available AI models and providers accessible through the Backbone AI gateway.", {}, async () => {
86
+ try {
87
+ const { data } = await client.GET("/v1/models");
88
+ const result = data;
89
+ const lines = (result.data ?? []).map((m) => `${m.id} (${m.owned_by})`);
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: `Available models:\n${lines.join("\n")}`,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ catch (error) {
100
+ return {
101
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
102
+ isError: true,
103
+ };
104
+ }
105
+ });
106
+ }
107
+ //# sourceMappingURL=ai-gateway.js.map
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function register(server: McpServer, client: ApiClient): void;
4
+ //# sourceMappingURL=conversion.d.ts.map
@@ -0,0 +1,197 @@
1
+ import { z } from "zod";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
+ import { formatErrorForMcp } from "../errors.js";
5
+ import { getMimeType } from "../mime.js";
6
+ /**
7
+ * Map user-friendly format names to backend OutputFormat enum values.
8
+ */
9
+ function mapOutputFormat(format) {
10
+ const map = {
11
+ markdown: "MD", md: "MD",
12
+ text: "TEXT", txt: "TEXT",
13
+ json: "JSON",
14
+ html: "HTML",
15
+ };
16
+ return map[format.toLowerCase()] ?? format.toUpperCase();
17
+ }
18
+ /**
19
+ * Extract content from an ExportDocument that uses per-format content fields.
20
+ */
21
+ function formatExportDocument(doc) {
22
+ const parts = [];
23
+ if (doc.mdContent)
24
+ parts.push({ type: "text", text: `--- ${doc.filename} (markdown) ---\n${doc.mdContent}` });
25
+ if (doc.textContent)
26
+ parts.push({ type: "text", text: `--- ${doc.filename} (text) ---\n${doc.textContent}` });
27
+ if (doc.htmlContent)
28
+ parts.push({ type: "text", text: `--- ${doc.filename} (html) ---\n${doc.htmlContent}` });
29
+ if (doc.jsonContent)
30
+ parts.push({ type: "text", text: `--- ${doc.filename} (json) ---\n${JSON.stringify(doc.jsonContent, null, 2)}` });
31
+ return parts;
32
+ }
33
+ export function register(server, client) {
34
+ // ── convert_document ────────────────────────────────────────────────────
35
+ server.tool("backbone_convert_document", "Convert documents to Markdown, text, HTML, or JSON. Accepts URLs, base64 data, or local file paths. Local files are automatically read and base64-encoded. Use async mode for large documents.", {
36
+ sources: z
37
+ .array(z.object({
38
+ type: z.enum(["url", "base64", "file"]).describe("Source type: 'url' for HTTP URLs, 'base64' for base64-encoded data, 'file' for local file paths"),
39
+ url: z.string().optional().describe("URL of the document (when type='url')"),
40
+ data: z.string().optional().describe("Base64-encoded document data (when type='base64')"),
41
+ filename: z.string().optional().describe("Filename (required for base64/file types)"),
42
+ path: z.string().optional().describe("Local file path (when type='file')"),
43
+ }))
44
+ .describe("List of document sources to convert"),
45
+ outputFormats: z
46
+ .array(z.string())
47
+ .optional()
48
+ .describe("Output formats, e.g. ['markdown', 'text', 'html', 'json']"),
49
+ pageRange: z
50
+ .string()
51
+ .optional()
52
+ .describe("Page range to convert, e.g. '1-5'"),
53
+ pipeline: z
54
+ .string()
55
+ .optional()
56
+ .describe("Processing pipeline: 'standard' (default) or 'vlm' (vision language model for image-heavy documents)"),
57
+ async: z
58
+ .boolean()
59
+ .optional()
60
+ .default(false)
61
+ .describe("If true, submit as async task and return task ID for polling"),
62
+ }, async ({ sources, outputFormats, pageRange, pipeline, async: isAsync }) => {
63
+ try {
64
+ const apiSources = [];
65
+ for (const src of sources) {
66
+ if (src.type === "url") {
67
+ if (!src.url)
68
+ throw new Error("url is required for type='url'");
69
+ apiSources.push({ kind: "HttpSource", url: src.url });
70
+ }
71
+ else if (src.type === "base64") {
72
+ if (!src.data || !src.filename)
73
+ throw new Error("data and filename are required for type='base64'");
74
+ apiSources.push({ kind: "Base64Source", content: src.data, filename: src.filename, mimeType: getMimeType(src.filename) });
75
+ }
76
+ else if (src.type === "file") {
77
+ const filePath = src.path ?? src.filename;
78
+ if (!filePath)
79
+ throw new Error("path or filename is required for type='file'");
80
+ const fileBuffer = await readFile(filePath);
81
+ const b64 = fileBuffer.toString("base64");
82
+ const name = src.filename ?? basename(filePath);
83
+ apiSources.push({ kind: "Base64Source", content: b64, filename: name, mimeType: getMimeType(name) });
84
+ }
85
+ }
86
+ const body = {
87
+ sources: apiSources,
88
+ options: {
89
+ ...(outputFormats ? { toFormats: outputFormats.map(mapOutputFormat) } : {}),
90
+ ...(pageRange ? { pageRange } : {}),
91
+ ...(pipeline ? { pipeline } : {}),
92
+ },
93
+ };
94
+ if (isAsync) {
95
+ const { data } = await client.POST("/v1/convert/source/async", { body });
96
+ return {
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: JSON.stringify(data, null, 2),
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ const { data } = await client.POST("/v1/convert/source", { body });
106
+ const convertResult = data;
107
+ const parts = [];
108
+ if (convertResult.documents?.length) {
109
+ for (const doc of convertResult.documents) {
110
+ parts.push(...formatExportDocument(doc));
111
+ }
112
+ }
113
+ if (convertResult.errors?.length) {
114
+ parts.push({
115
+ type: "text",
116
+ text: `Errors:\n${convertResult.errors.map((e) => `- ${e.filename ?? "unknown"}: ${e.errorMessage}`).join("\n")}`,
117
+ });
118
+ }
119
+ if (parts.length === 0) {
120
+ parts.push({
121
+ type: "text",
122
+ text: `Conversion completed with status: ${convertResult.status}`,
123
+ });
124
+ }
125
+ return { content: parts };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
130
+ isError: true,
131
+ };
132
+ }
133
+ });
134
+ // ── get_task_status ─────────────────────────────────────────────────────
135
+ server.tool("backbone_get_task_status", "Check the status of an async conversion task. Supports long polling with the wait parameter.", {
136
+ taskId: z.string().describe("The async task ID returned by convert_document"),
137
+ wait: z
138
+ .number()
139
+ .optional()
140
+ .describe("Long-poll timeout in seconds (max 60)"),
141
+ }, async ({ taskId, wait }) => {
142
+ try {
143
+ const { data } = await client.GET("/v1/convert/tasks/{taskId}", {
144
+ params: {
145
+ path: { taskId },
146
+ query: { wait },
147
+ },
148
+ });
149
+ return {
150
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
151
+ };
152
+ }
153
+ catch (error) {
154
+ return {
155
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
156
+ isError: true,
157
+ };
158
+ }
159
+ });
160
+ // ── get_task_result ─────────────────────────────────────────────────────
161
+ server.tool("backbone_get_task_result", "Get the result of a completed async conversion task.", {
162
+ taskId: z.string().describe("The async task ID"),
163
+ }, async ({ taskId }) => {
164
+ try {
165
+ const { data } = await client.GET("/v1/convert/tasks/{taskId}/result", {
166
+ params: { path: { taskId } },
167
+ });
168
+ const result = data;
169
+ const parts = [];
170
+ if (result.documents?.length) {
171
+ for (const doc of result.documents) {
172
+ parts.push(...formatExportDocument(doc));
173
+ }
174
+ }
175
+ if (result.errors?.length) {
176
+ parts.push({
177
+ type: "text",
178
+ text: `Errors:\n${result.errors.map((e) => `- ${e.filename ?? "unknown"}: ${e.errorMessage}`).join("\n")}`,
179
+ });
180
+ }
181
+ if (parts.length === 0) {
182
+ parts.push({
183
+ type: "text",
184
+ text: `Task result status: ${result.status}`,
185
+ });
186
+ }
187
+ return { content: parts };
188
+ }
189
+ catch (error) {
190
+ return {
191
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
192
+ isError: true,
193
+ };
194
+ }
195
+ });
196
+ }
197
+ //# sourceMappingURL=conversion.js.map
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function register(server: McpServer, _client: ApiClient, baseUrl: string, apiKey: string): void;
4
+ //# sourceMappingURL=docs.d.ts.map
@@ -0,0 +1,171 @@
1
+ import { z } from "zod";
2
+ import { formatErrorForMcp } from "../errors.js";
3
+ /* eslint-enable @typescript-eslint/no-explicit-any */
4
+ let specCache = null;
5
+ async function fetchSpec(baseUrl, apiKey) {
6
+ if (specCache)
7
+ return specCache;
8
+ const specUrl = `${baseUrl.replace(/\/+$/, "")}/v3/api-docs`;
9
+ const res = await fetch(specUrl, {
10
+ headers: { Authorization: `Bearer ${apiKey}` },
11
+ });
12
+ if (!res.ok) {
13
+ if (res.status === 404 || res.status === 403) {
14
+ throw new Error("API documentation is not available. Enable it by setting SPRINGDOC_ENABLED=true on the backend.");
15
+ }
16
+ throw new Error(`Failed to fetch API docs: ${res.status} ${res.statusText}`);
17
+ }
18
+ specCache = await res.json();
19
+ return specCache;
20
+ }
21
+ /**
22
+ * Collect all $ref strings from an object tree.
23
+ */
24
+ function collectRefs(obj, refs) {
25
+ if (obj === null || obj === undefined || typeof obj !== "object")
26
+ return;
27
+ if (Array.isArray(obj)) {
28
+ for (const item of obj)
29
+ collectRefs(item, refs);
30
+ return;
31
+ }
32
+ const record = obj;
33
+ if (typeof record["$ref"] === "string") {
34
+ refs.add(record["$ref"]);
35
+ }
36
+ for (const value of Object.values(record)) {
37
+ collectRefs(value, refs);
38
+ }
39
+ }
40
+ /**
41
+ * Recursively resolve schema refs — the initial set plus any schemas they reference.
42
+ */
43
+ function resolveSchemas(refPaths, allSchemas) {
44
+ const resolved = {};
45
+ const queue = [...refPaths];
46
+ const visited = new Set();
47
+ while (queue.length > 0) {
48
+ const ref = queue.pop();
49
+ if (visited.has(ref))
50
+ continue;
51
+ visited.add(ref);
52
+ const prefix = "#/components/schemas/";
53
+ if (!ref.startsWith(prefix))
54
+ continue;
55
+ const name = ref.slice(prefix.length);
56
+ const schema = allSchemas[name];
57
+ if (!schema)
58
+ continue;
59
+ resolved[name] = schema;
60
+ // Find nested refs in this schema
61
+ const nested = new Set();
62
+ collectRefs(schema, nested);
63
+ for (const n of nested) {
64
+ if (!visited.has(n))
65
+ queue.push(n);
66
+ }
67
+ }
68
+ return resolved;
69
+ }
70
+ export function register(server, _client, baseUrl, apiKey) {
71
+ // ── list_api_doc_sections ───────────────────────────────────────────────
72
+ server.tool("backbone_list_api_doc_sections", "List available API documentation sections (tags) from the OpenAPI spec. Use this to discover which sections you can fetch detailed docs for.", {}, async () => {
73
+ try {
74
+ const spec = await fetchSpec(baseUrl, apiKey);
75
+ const tags = spec.tags ?? [];
76
+ const paths = spec.paths ?? {};
77
+ // Count endpoints per tag
78
+ const tagCounts = new Map();
79
+ for (const methods of Object.values(paths)) {
80
+ for (const operation of Object.values(methods)) {
81
+ if (operation?.tags) {
82
+ for (const tag of operation.tags) {
83
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ const lines = tags.map((t) => {
89
+ const count = tagCounts.get(t.name) ?? 0;
90
+ const desc = t.description ? ` — ${t.description}` : "";
91
+ return `- ${t.name} (${count} endpoint${count !== 1 ? "s" : ""})${desc}`;
92
+ });
93
+ const info = spec.info ?? {};
94
+ const header = `${info.title ?? "API"} v${info.version ?? "?"}`;
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: `${header}\n\nAvailable sections:\n${lines.join("\n") || "(none)"}`,
100
+ },
101
+ ],
102
+ };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
107
+ isError: true,
108
+ };
109
+ }
110
+ });
111
+ // ── get_api_docs ────────────────────────────────────────────────────────
112
+ server.tool("backbone_get_api_docs", "Fetch API documentation for a specific section (tag). Returns the filtered endpoints and their referenced schemas. Use backbone_list_api_doc_sections first to see available tags.", {
113
+ tag: z.string().describe("The tag/section name to filter by (e.g. 'Projects', 'Extractions')"),
114
+ }, async ({ tag }) => {
115
+ try {
116
+ const spec = await fetchSpec(baseUrl, apiKey);
117
+ const allTags = spec.tags ?? [];
118
+ const tagNames = allTags.map((t) => t.name);
119
+ if (!tagNames.includes(tag)) {
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: `Tag "${tag}" not found. Available tags: ${tagNames.join(", ")}`,
125
+ },
126
+ ],
127
+ isError: true,
128
+ };
129
+ }
130
+ const allPaths = spec.paths ?? {};
131
+ const allSchemas = spec.components?.schemas ?? {};
132
+ // Filter paths to only operations matching the requested tag
133
+ const filteredPaths = {};
134
+ const refs = new Set();
135
+ for (const [path, methods] of Object.entries(allPaths)) {
136
+ const filteredMethods = {};
137
+ for (const [method, operation] of Object.entries(methods)) {
138
+ const op = operation;
139
+ if (op?.tags?.includes(tag)) {
140
+ filteredMethods[method] = operation;
141
+ collectRefs(operation, refs);
142
+ }
143
+ }
144
+ if (Object.keys(filteredMethods).length > 0) {
145
+ filteredPaths[path] = filteredMethods;
146
+ }
147
+ }
148
+ // Resolve referenced schemas (including transitive refs)
149
+ const referencedSchemas = resolveSchemas(refs, allSchemas);
150
+ const result = {
151
+ paths: filteredPaths,
152
+ schemas: referencedSchemas,
153
+ };
154
+ return {
155
+ content: [
156
+ {
157
+ type: "text",
158
+ text: JSON.stringify(result, null, 2),
159
+ },
160
+ ],
161
+ };
162
+ }
163
+ catch (error) {
164
+ return {
165
+ content: [{ type: "text", text: formatErrorForMcp(error) }],
166
+ isError: true,
167
+ };
168
+ }
169
+ });
170
+ }
171
+ //# sourceMappingURL=docs.js.map
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function register(server: McpServer, client: ApiClient): void;
4
+ //# sourceMappingURL=extraction.d.ts.map