Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,14 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **Firecrawl**: Scrape URL, Search Web
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
- **Linear**: Create Ticket, Find Issues
- **Mixedbread**: Ingest File
- **Perplexity**: Search Web, Ask Question, Research Topic
- **Resend**: Send Email
- **Slack**: Send Slack Message
- **Stripe**: Create Customer, Get Customer, Create Invoice
- **Superagent**: Guard, Redact
- **v0**: Create Chat, Send Message
- **Webflow**: List Sites, Get Site, Publish Site
<!-- PLUGINS:END -->

## Code Generation
Expand Down
1 change: 1 addition & 0 deletions plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import "./fal";
import "./firecrawl";
import "./github";
import "./linear";
import "./mixedbread";
import "./perplexity";
import "./resend";
import "./slack";
Expand Down
3 changes: 3 additions & 0 deletions plugins/mixedbread/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type MixedbreadCredentials = {
MIXEDBREAD_API_KEY?: string;
};
77 changes: 77 additions & 0 deletions plugins/mixedbread/icon.tsx

Large diffs are not rendered by default.

96 changes: 96 additions & 0 deletions plugins/mixedbread/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
import { MixedbreadIcon } from "./icon";

const mixedbreadPlugin: IntegrationPlugin = {
type: "mixedbread",
label: "Mixedbread",
description: "Upload files to Mixedbread document stores",

icon: MixedbreadIcon,

formFields: [
{
id: "mixedbreadApiKey",
label: "API Key",
type: "password",
placeholder: "mxbai-...",
configKey: "mixedbreadApiKey",
envVar: "MIXEDBREAD_API_KEY",
helpText: "Get your API key from ",
helpLink: {
text: "mixedbread.com",
url: "https://www.mixedbread.com/dashboard/api-keys",
},
},
],

testConfig: {
getTestFunction: async () => {
const { testMixedbread } = await import("./test");
return testMixedbread;
},
},

actions: [
{
slug: "ingest-file",
label: "Ingest File",
description: "Upload a file to a Mixedbread document store",
category: "Mixedbread",
stepFunction: "mixedbreadIngestFileStep",
stepImportPath: "ingest-file",
outputFields: [
{ field: "fileId", description: "The ID of the uploaded file" },
{ field: "status", description: "The status of the upload" },
],
configFields: [
{
key: "storeIdentifier",
label: "Store Identifier",
type: "template-input",
placeholder: "my-store or {{NodeName.storeId}}",
example: "my-document-store",
required: true,
},
{
key: "externalId",
label: "External ID",
type: "template-input",
placeholder: "unique-id or {{NodeName.id}}",
example: "doc-123",
required: true,
},
{
key: "content",
label: "Content",
type: "template-textarea",
placeholder: "File content or {{NodeName.content}}",
example: "Document content here...",
required: true,
rows: 5,
},
{
key: "mimetype",
label: "MIME Type",
type: "template-input",
placeholder: "text/plain or {{NodeName.mimetype}}",
example: "text/plain",
defaultValue: "text/plain",
required: true,
},
{
key: "metadata",
label: "Metadata (JSON)",
type: "template-textarea",
placeholder: '{"key": "value"} or {{NodeName.metadata}}',
example: '{"source": "workflow", "category": "docs"}',
rows: 3,
},
],
},
],
};

registerIntegration(mixedbreadPlugin);
export default mixedbreadPlugin;
113 changes: 113 additions & 0 deletions plugins/mixedbread/steps/ingest-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import "server-only";

import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import type { MixedbreadCredentials } from "../credentials";

const MIXEDBREAD_API_URL = "https://api.mixedbread.com/v1";

type IngestFileResult =
| { success: true; fileId: string; status: string }
| { success: false; error: string };

export type MixedbreadIngestFileCoreInput = {
storeIdentifier: string;
externalId: string;
content: string;
mimetype: string;
metadata?: string;
};

export type MixedbreadIngestFileInput = StepInput &
MixedbreadIngestFileCoreInput & {
integrationId?: string;
};

async function stepHandler(
input: MixedbreadIngestFileCoreInput,
credentials: MixedbreadCredentials
): Promise<IngestFileResult> {
const apiKey = credentials.MIXEDBREAD_API_KEY;

if (!apiKey) {
return {
success: false,
error:
"MIXEDBREAD_API_KEY is not configured. Please add it in Project Integrations.",
};
}

let parsedMetadata: Record<string, unknown> | undefined;
if (input.metadata) {
try {
parsedMetadata = JSON.parse(input.metadata);
} catch {
return {
success: false,
error: "Invalid JSON in metadata field",
};
}
}

try {
const fileBlob = new Blob([input.content], { type: input.mimetype });
const formData = new FormData();

const params: Record<string, unknown> = {
external_id: input.externalId,
};
if (parsedMetadata) {
params.metadata = parsedMetadata;
}
formData.append("params", JSON.stringify(params));
formData.append("file", fileBlob, input.externalId);

const response = await fetch(
`${MIXEDBREAD_API_URL}/stores/${encodeURIComponent(input.storeIdentifier)}/files/upload`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
},
body: formData,
}
);

if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `HTTP ${response.status}: ${errorText}`,
};
}

const result = await response.json();

return {
success: true,
fileId: result.id,
status: result.status,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
success: false,
error: `Failed to ingest file: ${message}`,
};
}
}

export async function mixedbreadIngestFileStep(
input: MixedbreadIngestFileInput
): Promise<IngestFileResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

return withStepLogging(input, () => stepHandler(input, credentials));
}
mixedbreadIngestFileStep.maxRetries = 0;

export const _integrationType = "mixedbread";
36 changes: 36 additions & 0 deletions plugins/mixedbread/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export async function testMixedbread(credentials: Record<string, string>) {
try {
const apiKey = credentials.MIXEDBREAD_API_KEY;

if (!apiKey) {
return {
success: false,
error: "MIXEDBREAD_API_KEY is required",
};
}

// Make a lightweight API call to validate the key
const response = await fetch("https://api.mixedbread.com/v1/stores", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});

if (response.ok) {
return { success: true };
}

if (response.status === 401) {
return { success: false, error: "Invalid API key" };
}

const error = await response.text();
return { success: false, error: `API error: ${error}` };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}