Skip to content

Example of custom mcp server extensions #145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 19, 2025
Merged
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
11 changes: 6 additions & 5 deletions .speakeasy/gen.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ id: f42cb8e6-e2ce-4565-b975-5a9f38b94d5a
management:
docChecksum: 2e7f129641ad96d49fcda2ceb173d43c
docVersion: 1.0.81
speakeasyVersion: 1.517.3
generationVersion: 2.548.6
releaseVersion: 0.22.0
configChecksum: 0de256135a61cf1f1f4aa7e2b4cc8f93
speakeasyVersion: 1.519.0
generationVersion: 2.552.1
releaseVersion: 0.23.0
configChecksum: 86d5f752c6378b160f1e0a2fb1b0cea7
repoURL: https://github.com/Unstructured-IO/unstructured-js-client.git
repoSubDirectory: .
installationURL: https://github.com/Unstructured-IO/unstructured-js-client
Expand All @@ -24,7 +24,7 @@ features:
globalSecurity: 2.82.13
globalSecurityCallbacks: 0.1.0
globalServerURLs: 2.82.4
mcpServer: 0.7.0
mcpServer: 0.8.0
nameOverrides: 2.81.2
nullables: 0.1.1
openEnums: 0.1.1
Expand Down Expand Up @@ -88,6 +88,7 @@ generatedFiles:
- src/mcp-server/console-logger.ts
- src/mcp-server/extensions.ts
- src/mcp-server/mcp-server.ts
- src/mcp-server/prompts.ts
- src/mcp-server/resources.ts
- src/mcp-server/scopes.ts
- src/mcp-server/server.ts
Expand Down
9 changes: 4 additions & 5 deletions .speakeasy/workflow.lock
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
speakeasyVersion: 1.517.3
speakeasyVersion: 1.519.0
sources:
my-source:
sourceNamespace: my-source
sourceRevisionDigest: sha256:7b3ee52d5e5acc9bc97268e7996c203d4bec0181379eb5c95868c982fb820726
sourceRevisionDigest: sha256:acb76c58ac398f0d1ea1f9eef8df422c40d5a237c7cf020522258b327792a68d
sourceBlobDigest: sha256:822e38da0c61b9aeaa5446413a6cec058a464920a02d20d9ee7955c827446050
tags:
- latest
- speakeasy-sdk-regen-1742057884
- 1.0.81
targets:
unstructed-typescript:
source: my-source
sourceNamespace: my-source
sourceRevisionDigest: sha256:7b3ee52d5e5acc9bc97268e7996c203d4bec0181379eb5c95868c982fb820726
sourceRevisionDigest: sha256:acb76c58ac398f0d1ea1f9eef8df422c40d5a237c7cf020522258b327792a68d
sourceBlobDigest: sha256:822e38da0c61b9aeaa5446413a6cec058a464920a02d20d9ee7955c827446050
codeSamplesNamespace: my-source-typescript-code-samples
codeSamplesRevisionDigest: sha256:86e8bf47bf21198d030067fd18395449530c67061224d03d612d9f6de944baa4
codeSamplesRevisionDigest: sha256:bc9b9485931a5f01bce3a872cdba2295e5aa3e155f52890568c3e8b18c9fd66d
workflow:
workflowVersion: 1.0.0
speakeasyVersion: latest
Expand Down
2 changes: 1 addition & 1 deletion gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ generation:
oAuth2ClientCredentialsEnabled: false
oAuth2PasswordEnabled: false
typescript:
version: 0.22.0
version: 0.23.0
additionalDependencies:
dependencies:
async: ^3.2.5
Expand Down
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{
"name": "unstructured-client",
"version": "0.22.0",
"version": "0.23.0",
"exports": {
".": "./src/index.ts",
"./sdk/models/errors": "./src/sdk/models/errors/index.ts",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unstructured-client",
"version": "0.22.0",
"version": "0.23.0",
"author": "Unstructured",
"type": "module",
"bin": {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export function serverURLFromOptions(options: SDKOptions): URL | null {
export const SDK_METADATA = {
language: "typescript",
openapiDocVersion: "1.0.81",
sdkVersion: "0.22.0",
genVersion: "2.548.6",
sdkVersion: "0.23.0",
genVersion: "2.552.1",
userAgent:
"speakeasy-sdk/typescript 0.22.0 2.548.6 1.0.81 unstructured-client",
"speakeasy-sdk/typescript 0.23.0 2.552.1 1.0.81 unstructured-client",
} as const;
4 changes: 4 additions & 0 deletions src/mcp-server/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
*/

import { ZodRawShape } from "zod";
import { PromptArgsRawShape, PromptDefinition } from "./prompts.js";
import { ResourceDefinition, ResourceTemplateDefinition } from "./resources.js";
import { ToolDefinition } from "./tools.js";

export type Register = {
tool: <A extends ZodRawShape | undefined>(def: ToolDefinition<A>) => void;
resource: (def: ResourceDefinition) => void;
resourceTemplate: (def: ResourceTemplateDefinition) => void;
prompt: <A extends PromptArgsRawShape | undefined>(
prompt: PromptDefinition<A>,
) => void;
};
2 changes: 1 addition & 1 deletion src/mcp-server/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const routes = buildRouteMap({
export const app = buildApplication(routes, {
name: "mcp",
versionInfo: {
currentVersion: "0.22.0",
currentVersion: "0.23.0",
},
});

Expand Down
110 changes: 110 additions & 0 deletions src/mcp-server/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
*/

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import { GetPromptResult } from "@modelcontextprotocol/sdk/types.js";
import {
objectOutputType,
ZodOptional,
ZodType,
ZodTypeAny,
ZodTypeDef,
} from "zod";
import { UnstructuredClientCore } from "../core.js";
import { ConsoleLogger } from "./console-logger.js";
import { MCPScope } from "./scopes.js";

// '@modelcontextprotocol/sdk' currently does not export this type
export type PromptArgsRawShape = {
[k: string]:
| ZodType<string, ZodTypeDef, string>
| ZodOptional<ZodType<string, ZodTypeDef, string>>;
};

export type PromptDefinition<
Args extends undefined | PromptArgsRawShape = undefined,
> = Args extends PromptArgsRawShape ? {
name: string;
description?: string;
scopes?: MCPScope[];
args: Args;
prompt: (
client: UnstructuredClientCore,
args: objectOutputType<Args, ZodTypeAny>,
extra: RequestHandlerExtra,
) => GetPromptResult | Promise<GetPromptResult>;
}
: {
name: string;
description?: string;
scopes?: MCPScope[];
args?: undefined;
prompt: (
client: UnstructuredClientCore,
extra: RequestHandlerExtra,
) => GetPromptResult | Promise<GetPromptResult>;
};

// Optional function to assist with formatting prompt results
export async function formatResult(value: string): Promise<GetPromptResult> {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: value,
},
},
],
};
}

export function createRegisterPrompt(
logger: ConsoleLogger,
server: McpServer,
sdk: UnstructuredClientCore,
allowedScopes: Set<MCPScope>,
): <A extends PromptArgsRawShape | undefined>(
prompt: PromptDefinition<A>,
) => void {
return <A extends PromptArgsRawShape | undefined>(
prompt: PromptDefinition<A>,
): void => {
const scopes = prompt.scopes ?? [];
if (!scopes.every((s: MCPScope) => allowedScopes.has(s))) {
return;
}

if (prompt.args) {
if (prompt.description) {
server.prompt(
prompt.name,
prompt.description,
prompt.args,
async (args, ctx) => prompt.prompt(sdk, args, ctx),
);
} else {
server.prompt(
prompt.name,
prompt.args,
async (args, ctx) => prompt.prompt(sdk, args, ctx),
);
}
} else {
if (prompt.description) {
server.prompt(
prompt.name,
prompt.description,
async (ctx) => prompt.prompt(sdk, ctx),
);
} else {
server.prompt(prompt.name, async (ctx) => prompt.prompt(sdk, ctx));
}
}

logger.debug("Registered prompt", { name: prompt.name });
};
}
1 change: 1 addition & 0 deletions src/mcp-server/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type ResourceTemplateDefinition = {
read: ReadResourceTemplateCallback;
};

// Optional function to assist with formatting resource results
export async function formatResult(
value: unknown,
uri: URL,
Expand Down
93 changes: 93 additions & 0 deletions src/mcp-server/server.extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { formatResult, ToolDefinition } from "./tools.js";
import { Register } from "./extensions.js";
import { generalPartition } from "../funcs/generalPartition.js";
import fs from 'node:fs/promises';
import { UnstructuredClientCore } from "../core.js";
import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import { z } from "zod";
import { Strategy, Strategy$inboundSchema, StrategyOpen } from "../sdk/models/shared/partitionparameters.js";


type FileRequest = {
strategy?: StrategyOpen | undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can and probably should expose more args here. This was just a quick demo I spun up

file_content?: string | undefined;
file_path?: string | undefined;
file_name: string;
};

const FileRequest$inboundSchema: z.ZodType<
FileRequest,
z.ZodTypeDef,
unknown
> = z.object({
file_content: z.string().optional(),
file_path: z.string().optional(),
file_name: z.string(),
strategy: Strategy$inboundSchema.default(Strategy.HiRes),
});

const customToolArg = {
request: FileRequest$inboundSchema
};



export const tool$generalPartitionCorrect: ToolDefinition<typeof customToolArg> = {
name: "correct_general-partition",
description: `use this tool to pass a file to unstructured. You must BASE64 ENCODE uploaded file content before passing to unstructured. Alternatively, if the user did not upload a file they can provide a full local file path. `,
args: customToolArg,
tool: async (client: UnstructuredClientCore, args, ctx: RequestHandlerExtra) => {
let data: Uint8Array;
if (args.request.file_content) {
try {
data = new Uint8Array(Buffer.from(args.request.file_content, 'base64'));
} catch (e) {
return {
content: [{
type: "text",
text: `You must BASE64 encode this file content then pass it to the tool.`,
}],
isError: true,
};
}
} else if (args.request.file_path) {
data = new Uint8Array(await fs.readFile(args.request.file_path));
} else {
return {
content: [{
type: "text",
text: `A full file path for file content must be provided`,
}],
isError: true,
};
}
const [result, apiCall] = await generalPartition(
client,
{
partitionParameters: {
files: {
content: data,
fileName: args.request.file_name,
},
strategy: args.request.strategy,
}
},
{ fetchOptions: { signal: ctx.signal } },
).$inspect();

if (!result.ok) {
return {
content: [{ type: "text", text: result.error.message }],
isError: true,
};
}

const value = result.value;

return formatResult(value, apiCall);
},
};

export function registerMCPExtensions(register: Register): void {
register.tool(tool$generalPartitionCorrect);
}
10 changes: 8 additions & 2 deletions src/mcp-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { UnstructuredClientCore } from "../core.js";
import { SDKOptions } from "../lib/config.js";
import type { ConsoleLogger } from "./console-logger.js";
import { Register } from "./extensions.js";
import { createRegisterPrompt } from "./prompts.js";
import {
createRegisterResource,
createRegisterResourceTemplate,
} from "./resources.js";
import { MCPScope, mcpScopes } from "./scopes.js";
import { registerMCPExtensions } from "./server.extensions.js";
import { createRegisterTool } from "./tools.js";
import { tool$generalPartition } from "./tools/generalPartition.js";

Expand All @@ -24,7 +27,7 @@ export function createMCPServer(deps: {
}) {
const server = new McpServer({
name: "UnstructuredClient",
version: "0.22.0",
version: "0.23.0",
});

const client = new UnstructuredClientCore({
Expand All @@ -50,10 +53,13 @@ export function createMCPServer(deps: {
client,
scopes,
);
const register = { tool, resource, resourceTemplate };
const prompt = createRegisterPrompt(deps.logger, server, client, scopes);
const register = { tool, resource, resourceTemplate, prompt };
void register; // suppress unused warnings

tool(tool$generalPartition);

registerMCPExtensions(register satisfies Register);

return server;
}
1 change: 1 addition & 0 deletions src/mcp-server/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type ToolDefinition<Args extends undefined | ZodRawShape = undefined> =
) => CallToolResult | Promise<CallToolResult>;
};

// Optional function to assist with formatting tool results
export async function formatResult(
value: unknown,
init: { response?: Response | undefined },
Expand Down