exactmcp
Version:
MCP server for Exact Online API integration
277 lines • 12 kB
JavaScript
#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const exact_client_js_1 = require("./exact-client.js");
const dotenv = __importStar(require("dotenv"));
dotenv.config();
const server = new index_js_1.Server({
name: 'exact-mcp-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
let exactClient = null;
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'authenticate',
description: 'Authenticate with Exact Online API using OAuth2',
inputSchema: {
type: 'object',
properties: {
clientId: {
type: 'string',
description: 'Exact Online App Client ID (optional if set in .env)',
},
clientSecret: {
type: 'string',
description: 'Exact Online App Client Secret (optional if set in .env)',
},
redirectUri: {
type: 'string',
description: 'OAuth redirect URI',
default: 'http://localhost:8080/callback',
},
},
required: [],
},
},
{
name: 'complete_auth',
description: 'Complete OAuth authentication with authorization code',
inputSchema: {
type: 'object',
properties: {
authCode: {
type: 'string',
description: 'Authorization code from OAuth callback',
},
},
required: ['authCode'],
},
},
{
name: 'get_sales_orders',
description: 'Retrieve sales orders from Exact Online',
inputSchema: {
type: 'object',
properties: {
divisionCode: {
type: 'string',
description: 'Division code (company database)',
},
filter: {
type: 'string',
description: 'OData filter query (optional)',
},
select: {
type: 'string',
description: 'OData select fields (optional)',
},
top: {
type: 'number',
description: 'Number of records to return (default: 50)',
default: 50,
},
},
required: ['divisionCode'],
},
},
{
name: 'get_items',
description: 'Retrieve items from Exact Online',
inputSchema: {
type: 'object',
properties: {
divisionCode: {
type: 'string',
description: 'Division code (company database)',
},
filter: {
type: 'string',
description: 'OData filter query (optional)',
},
select: {
type: 'string',
description: 'OData select fields (optional)',
},
top: {
type: 'number',
description: 'Number of records to return (default: 50)',
default: 50,
},
},
required: ['divisionCode'],
},
},
{
name: 'get_accounts',
description: 'Retrieve accounts (customers/suppliers) from Exact Online',
inputSchema: {
type: 'object',
properties: {
divisionCode: {
type: 'string',
description: 'Division code (company database)',
},
filter: {
type: 'string',
description: 'OData filter query (optional)',
},
select: {
type: 'string',
description: 'OData select fields (optional)',
},
top: {
type: 'number',
description: 'Number of records to return (default: 50)',
default: 50,
},
},
required: ['divisionCode'],
},
},
],
};
});
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'authenticate': {
const { clientId = process.env.EXACT_CLIENT_ID, clientSecret = process.env.EXACT_CLIENT_SECRET, redirectUri = process.env.EXACT_REDIRECT_URI || 'http://localhost:8080/callback' } = args;
if (!clientId || !clientSecret) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'clientId and clientSecret are required (either as parameters or environment variables)');
}
exactClient = new exact_client_js_1.ExactOnlineClient(clientId, clientSecret, redirectUri);
const authUrl = exactClient.getAuthorizationUrl();
return {
content: [
{
type: 'text',
text: `Authentication initiated. Please visit the following URL to authorize:\n\n${authUrl}\n\nAfter authorization, you will be redirected. Copy the 'code' parameter from the redirect URL and use it with the 'complete_auth' tool.`,
},
],
};
}
case 'complete_auth': {
const { authCode } = args;
if (!exactClient) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Must authenticate first using the authenticate tool');
}
const tokenResult = await exactClient.exchangeCodeForToken(authCode);
const divisions = await exactClient.getDivisions();
return {
content: [
{
type: 'text',
text: `Authentication successful! Token expires at: ${new Date(tokenResult.expires_at).toISOString()}\n\nAvailable divisions:\n${divisions.map(d => `- ${d.code}: ${d.description}`).join('\n')}`,
},
],
};
}
case 'get_sales_orders': {
const { divisionCode, filter, select, top = 50 } = args;
if (!exactClient || !exactClient.isAuthenticated()) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Must authenticate first');
}
const orders = await exactClient.getSalesOrders(divisionCode, { filter, select, top });
return {
content: [
{
type: 'text',
text: `Retrieved ${orders.length} sales orders:\n\n${JSON.stringify(orders, null, 2)}`,
},
],
};
}
case 'get_items': {
const { divisionCode, filter, select, top = 50 } = args;
if (!exactClient || !exactClient.isAuthenticated()) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Must authenticate first');
}
const items = await exactClient.getItems(divisionCode, { filter, select, top });
return {
content: [
{
type: 'text',
text: `Retrieved ${items.length} items:\n\n${JSON.stringify(items, null, 2)}`,
},
],
};
}
case 'get_accounts': {
const { divisionCode, filter, select, top = 50 } = args;
if (!exactClient || !exactClient.isAuthenticated()) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Must authenticate first');
}
const accounts = await exactClient.getAccounts(divisionCode, { filter, select, top });
return {
content: [
{
type: 'text',
text: `Retrieved ${accounts.length} accounts:\n\n${JSON.stringify(accounts, null, 2)}`,
},
],
};
}
default:
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof types_js_1.McpError) {
throw error;
}
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Tool execution failed: ${error}`);
}
});
async function main() {
const transport = new stdio_js_1.StdioServerTransport();
await server.connect(transport);
console.error('Exact MCP server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
//# sourceMappingURL=index.js.map