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
54 changes: 54 additions & 0 deletions src/contract/get_contract_download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { SpotDraftClient } from '../spotdraft_client.js';

const getContractDownloadTool: Tool = {
name: 'get_contract_download',
description: `Downloads a copy of the contract file directly from SpotDraft using API v2.1.

Returns the contract file as base64-encoded data along with metadata such as filename and content type.

## Parameters
- \`composite_id\`: ID of the Contract. Should be of the form T-123 or H-123. T stands for Template contracts and H stands for Historical contracts.

## Response
Returns an object containing:
- \`data\`: Base64-encoded file content
- \`filename\`: Original filename (if available from response headers)
- \`contentType\`: MIME type of the file (if available from response headers)

### How the Model Should Use the Output
The \`data\` field contains the raw binary content of the contract file. To help the user:
- If the platform supports file uploads (e.g., Slack, email, chat app), use \`data\`, \`filename\`, and \`contentType\` to send the contract as a file attachment.
- If direct file sharing is not supported, respond with a message indicating the file was downloaded and provide its name and type.
- Do not attempt to display or summarize the file content.
- Do not try to decode or transform the binary string.

### Usage Example
Given \`composite_id = "T-332728"\`, the model can call this tool to retrieve and forward the finalized contract to the user as a downloadable file.`,

inputSchema: {
type: 'object',
properties: {
composite_id: {
type: 'string',
description:
'ID of the Contract (e.g., T-123 or H-123). T stands for Template contracts and H stands for Historical contracts.',
},
},
required: ['composite_id'],
},
};

interface GetContractDownloadRequest {
composite_id: string;
}

const getContractDownload = async (
request: GetContractDownloadRequest,
spotdraftClient: SpotDraftClient,
): Promise<{ data: string; filename?: string; contentType?: string }> => {
const endpoint = `/v2.1/public/contracts/${request.composite_id}/download`;
return spotdraftClient.downloadFile(endpoint);
};

export { getContractDownload, GetContractDownloadRequest, getContractDownloadTool };
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import {
getContractApprovals,
getContractApprovalsTool,
} from './contract/get_contract_approvals.js';
import {
GetContractDownloadRequest,
getContractDownload,
getContractDownloadTool,
} from './contract/get_contract_download.js';
import {
GetContractDownloadLinkRequest,
getContractDownloadLink,
Expand Down Expand Up @@ -69,6 +74,7 @@ async function handleListToolsRequest() {
tools: [
getContractListTool,
getContractDownloadLinkTool,
getContractDownloadTool,
getContractStatusTool,
getContractActivityLogTool,
getContractApprovalsTool,
Expand Down Expand Up @@ -119,6 +125,9 @@ async function handleCallToolRequest(request: CallToolRequest, spotdraftClient:
case getContractDownloadLinkTool.name:
result = await getContractDownloadLink(args as unknown as GetContractDownloadLinkRequest, spotdraftClient);
break;
case getContractDownloadTool.name:
result = await getContractDownload(args as unknown as GetContractDownloadRequest, spotdraftClient);
break;
case getContractStatusTool.name:
result = await getContractStatus(args as unknown as GetContractStatusRequest, spotdraftClient);
break;
Expand Down
29 changes: 29 additions & 0 deletions src/spotdraft_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,35 @@ export class SpotDraftClient {
}
return response.json();
}

async downloadFile(endpoint: string): Promise<{ data: string; filename?: string; contentType?: string }> {
const response = await this.post(endpoint, {});

if (!response.ok) {
const errorBody = await response.text();
throw new Error(`SpotDraft API Error: ${response.status} ${response.statusText} - ${errorBody}`);
}
Comment on lines +52 to +58
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix critical logic error in downloadFile method.

The method calls this.post(endpoint, {}) which returns a parsed JSON response, but then tries to check response.ok on that result. The post() method already handles response validation and returns the parsed response body, not the raw Response object.

 async downloadFile(endpoint: string): Promise<{ data: string; filename?: string; contentType?: string }> {
-   const response = await this.post(endpoint, {});
-
-   if (!response.ok) {
-     const errorBody = await response.text();
-     throw new Error(`SpotDraft API Error: ${response.status} ${response.statusText} - ${errorBody}`);
-   }
+   const response = await fetch(`${this.baseUrl}${endpoint}`, {
+     method: 'POST',
+     headers: this.headers,
+     body: JSON.stringify({}),
+   });
+
+   if (!response.ok) {
+     const errorBody = await response.text();
+     throw new Error(`SpotDraft API Error: ${response.status} ${response.statusText} - ${errorBody}`);
+   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async downloadFile(endpoint: string): Promise<{ data: string; filename?: string; contentType?: string }> {
const response = await this.post(endpoint, {});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`SpotDraft API Error: ${response.status} ${response.statusText} - ${errorBody}`);
}
async downloadFile(endpoint: string): Promise<{ data: string; filename?: string; contentType?: string }> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: this.headers,
body: JSON.stringify({}),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`SpotDraft API Error: ${response.status} ${response.statusText} - ${errorBody}`);
}
🤖 Prompt for AI Agents
In src/spotdraft_client.ts around lines 52 to 58, the downloadFile method
incorrectly treats the result of this.post(endpoint, {}) as a raw Response
object and checks response.ok, but this.post returns parsed JSON and already
handles response validation. Remove the check for response.ok and the error
handling based on it, and directly use the returned parsed response data as
intended.


const arrayBuffer = await response.arrayBuffer();
const base64Data = Buffer.from(arrayBuffer).toString('base64');

const contentDisposition = response.headers.get('content-disposition');
let filename: string | undefined;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch && filenameMatch[1]) {
filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
}
}

const contentType = response.headers.get('content-type') || undefined;

return {
data: base64Data,
filename,
contentType,
};
}
}

export default SpotDraftClient;