#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import {
generateConsent,
consumeConsent,
getAuthState,
getAccessToken,
isTokenValid,
resetAuthState,
getStep2Instructions,
getFundLimit,
placeOrder,
modifyOrder,
cancelOrder,
getOrderBook,
getOrderByID,
getOrderByCorrelationID,
getTradeBook,
getOrderTrades,
placeSuperOrder,
modifySuperOrder,
cancelSuperOrderLeg,
getSuperOrderBook,
} from './authentication.js';
import { validateConfig } from './config.js';
// Validate configuration on startup
validateConfig();
// Create MCP Server instance with tools capability
const server = new Server({
name: 'dhan-mcp-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
// Define tool schemas
const tools = [
{
name: 'start_authentication',
description:
'Initiates the DhanHQ authentication flow (Step 1). Returns a login URL that you must open in your browser to authenticate.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
{
name: 'get_login_instructions',
description:
'Gets the login instructions and URL for Step 2 (browser-based login). You must complete this step manually by opening the URL in your browser.',
inputSchema: {
type: 'object' as const,
properties: {
consentAppId: {
type: 'string' as const,
description:
'The consentAppId from Step 1 (start_authentication). If not provided, the latest one from the current session will be used.',
},
},
required: [],
},
},
{
name: 'complete_authentication',
description:
'Completes the authentication flow (Step 3). Takes the tokenId from the redirect URL after browser login and exchanges it for an access token.',
inputSchema: {
type: 'object' as const,
properties: {
tokenId: {
type: 'string' as const,
description:
'The tokenId received in the redirect URL after browser login (Step 2).',
},
},
required: ['tokenId'],
},
},
{
name: 'check_auth_status',
description:
'Checks the current authentication status and returns details about the active access token if available.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
{
name: 'reset_authentication',
description: 'Clears the current authentication state and resets the session.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
{
name: 'get_fund_limit',
description:
'Retrieves account fund limit information including available balance, margins, collateral, and withdrawable balance. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
// ===== ORDER MANAGEMENT TOOLS =====
{
name: 'place_order',
description:
'Places a new order on DhanHQ. Requires authentication. Supports MARKET, LIMIT, STOP_LOSS, and STOP_LOSS_MARKET orders.',
inputSchema: {
type: 'object' as const,
properties: {
dhanClientId: { type: 'string', description: 'Your Dhan client ID' },
correlationId: { type: 'string', description: 'Unique correlation ID for order tracking' },
transactionType: { type: 'string', enum: ['BUY', 'SELL'] },
exchangeSegment: { type: 'string', description: 'e.g., NSE_EQ, BSE_EQ, NFO_FUT' },
productType: {
type: 'string',
enum: ['CNC', 'INTRADAY', 'MARGIN', 'MTF', 'CO', 'BO'],
description: 'CNC=Delivery, INTRADAY=Intraday, MARGIN=Margin, CO=Cover Order, BO=Bracket Order',
},
orderType: {
type: 'string',
enum: ['MARKET', 'LIMIT', 'STOP_LOSS', 'STOP_LOSS_MARKET'],
},
validity: {
type: 'string',
enum: ['DAY', 'IOC'],
description: 'DAY=Valid for today, IOC=Immediate or Cancel',
},
securityId: { type: 'string', description: 'Security ID for the instrument' },
quantity: { type: 'number', description: 'Quantity to order' },
price: { type: 'number', description: 'Price per share (required for LIMIT/STOP_LOSS)' },
triggerPrice: { type: 'number', description: 'Trigger price (required for STOP_LOSS/STOP_LOSS_MARKET)' },
disclosedQuantity: { type: 'number', description: 'Quantity visible in order book (min 30% of quantity)' },
afterMarketOrder: { type: 'boolean', description: 'Flag for after-market orders' },
amoTime: { type: 'string', enum: ['PRE_OPEN', 'OPEN', 'OPEN_30', 'OPEN_60'], description: 'AMO timing' },
boProfitValue: { type: 'number', description: 'BO target price change' },
boStopLossValue: { type: 'number', description: 'BO stop loss price change' },
},
required: [
'dhanClientId',
'correlationId',
'transactionType',
'exchangeSegment',
'productType',
'orderType',
'validity',
'securityId',
'quantity',
'price',
],
},
},
{
name: 'modify_order',
description:
'Modifies a pending order. Can change price, quantity, order type, and other parameters. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
orderId: { type: 'string', description: 'Order ID to modify' },
dhanClientId: { type: 'string', description: 'Your Dhan client ID' },
orderType: {
type: 'string',
enum: ['MARKET', 'LIMIT', 'STOP_LOSS', 'STOP_LOSS_MARKET'],
},
quantity: { type: 'number', description: 'New quantity' },
price: { type: 'number', description: 'New price (for LIMIT/STOP_LOSS)' },
triggerPrice: { type: 'number', description: 'New trigger price (for STOP_LOSS/STOP_LOSS_MARKET)' },
disclosedQuantity: { type: 'number', description: 'New disclosed quantity' },
},
required: ['orderId', 'dhanClientId', 'orderType'],
},
},
{
name: 'cancel_order',
description:
'Cancels a pending order. Requires authentication and a valid order ID.',
inputSchema: {
type: 'object' as const,
properties: {
orderId: { type: 'string', description: 'Order ID to cancel' },
},
required: ['orderId'],
},
},
{
name: 'get_order_book',
description:
'Retrieves all orders placed during the day with their current status. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
{
name: 'get_order_by_id',
description:
'Retrieves the details and status of a specific order by order ID. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
orderId: { type: 'string', description: 'Order ID to retrieve' },
},
required: ['orderId'],
},
},
{
name: 'get_order_by_correlation_id',
description:
'Retrieves order status using the user-specified correlation ID. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
correlationId: { type: 'string', description: 'Correlation ID used when placing the order' },
},
required: ['correlationId'],
},
},
{
name: 'get_trade_book',
description:
'Retrieves all trades executed during the day. Useful for tracking fills and execution prices. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
{
name: 'get_order_trades',
description:
'Retrieves all trades for a specific order. Useful for partial fills or bracket/cover orders. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
orderId: { type: 'string', description: 'Order ID to get trades for' },
},
required: ['orderId'],
},
},
// ===== SUPER ORDER MANAGEMENT TOOLS =====
{
name: 'place_super_order',
description:
'Places a smart super order combining entry, target, and stop-loss legs. Supports trailing stop loss. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
dhanClientId: { type: 'string', description: 'Your Dhan client ID' },
correlationId: { type: 'string', description: 'Unique correlation ID for order tracking' },
transactionType: { type: 'string', enum: ['BUY', 'SELL'] },
exchangeSegment: { type: 'string', description: 'e.g., NSE_EQ, BSE_EQ' },
productType: {
type: 'string',
enum: ['CNC', 'INTRADAY', 'MARGIN', 'MTF', 'CO', 'BO'],
},
orderType: {
type: 'string',
enum: ['MARKET', 'LIMIT', 'STOP_LOSS', 'STOP_LOSS_MARKET'],
},
securityId: { type: 'string', description: 'Security ID for the instrument' },
quantity: { type: 'number', description: 'Quantity for entry leg' },
price: { type: 'number', description: 'Entry price' },
targetPrice: { type: 'number', description: 'Target price for profit booking' },
stopLossPrice: { type: 'number', description: 'Stop loss price' },
trailingJump: {
type: 'number',
description: 'Trailing stop loss jump amount (optional)',
},
},
required: [
'dhanClientId',
'correlationId',
'transactionType',
'exchangeSegment',
'productType',
'orderType',
'securityId',
'quantity',
'price',
'targetPrice',
'stopLossPrice',
],
},
},
{
name: 'modify_super_order',
description:
'Modifies any leg of a pending or part-traded super order. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
orderId: { type: 'string', description: 'Super order ID to modify' },
dhanClientId: { type: 'string', description: 'Your Dhan client ID' },
legName: {
type: 'string',
enum: ['ENTRY_LEG', 'TARGET_LEG', 'STOP_LOSS_LEG'],
description: 'Which leg to modify',
},
orderType: {
type: 'string',
enum: ['MARKET', 'LIMIT', 'STOP', 'STOP_LIMIT'],
},
quantity: { type: 'number', description: 'New quantity' },
price: { type: 'number', description: 'New price' },
targetPrice: { type: 'number', description: 'New target price (for entry leg)' },
stopLossPrice: { type: 'number', description: 'New stop loss price' },
trailingJump: { type: 'number', description: 'New trailing jump amount' },
},
required: ['orderId', 'dhanClientId', 'legName', 'orderType'],
},
},
{
name: 'cancel_super_order_leg',
description:
'Cancels a specific leg of a super order (ENTRY_LEG, TARGET_LEG, or STOP_LOSS_LEG). Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {
orderId: { type: 'string', description: 'Super order ID' },
orderLeg: {
type: 'string',
enum: ['ENTRY_LEG', 'TARGET_LEG', 'STOP_LOSS_LEG'],
description: 'Which leg to cancel',
},
},
required: ['orderId', 'orderLeg'],
},
},
{
name: 'get_super_order_book',
description:
'Retrieves all super orders placed during the day with nested leg details. Requires authentication.',
inputSchema: {
type: 'object' as const,
properties: {},
required: [],
},
},
];
// Register list tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
// Register call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'start_authentication': {
console.error('[Tool] Executing: start_authentication');
const result = await generateConsent();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_login_instructions': {
console.error('[Tool] Executing: get_login_instructions');
const authState = getAuthState();
const consentAppId =
(args as Record<string, unknown>).consentAppId ||
authState.consentAppId;
if (!consentAppId) {
throw new Error(
'No consentAppId available. Please run start_authentication first.'
);
}
const instructions = getStep2Instructions(consentAppId as string);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(instructions, null, 2),
},
],
};
}
case 'complete_authentication': {
console.error('[Tool] Executing: complete_authentication');
const { tokenId } = args as Record<string, unknown>;
if (!tokenId) {
throw new Error('tokenId is required');
}
const authToken = await consumeConsent(tokenId as string);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
message: 'Authentication completed successfully',
authToken,
},
null,
2
),
},
],
};
}
case 'check_auth_status': {
console.error('[Tool] Executing: check_auth_status');
const authState = getAuthState();
const accessToken = getAccessToken();
const tokenValid = isTokenValid();
const status = {
authenticated: !!authState.authToken,
hasValidToken: tokenValid,
accessToken: accessToken ? '***REDACTED***' : null,
authState: {
consentAppId: authState.consentAppId || null,
consentAppStatus: authState.consentAppStatus || null,
tokenId: authState.tokenId ? '***REDACTED***' : null,
clientInfo: authState.authToken
? {
dhanClientId: authState.authToken.dhanClientId,
dhanClientName: authState.authToken.dhanClientName,
dhanClientUcc: authState.authToken.dhanClientUcc,
expiryTime: authState.authToken.expiryTime,
givenPowerOfAttorney:
authState.authToken.givenPowerOfAttorney,
generatedAt: authState.authToken.generatedAt,
}
: null,
},
};
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(status, null, 2),
},
],
};
}
case 'reset_authentication': {
console.error('[Tool] Executing: reset_authentication');
resetAuthState();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: true,
message: 'Authentication state has been reset',
},
null,
2
),
},
],
};
}
case 'get_fund_limit': {
console.error('[Tool] Executing: get_fund_limit');
const fundLimit = await getFundLimit();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(fundLimit, null, 2),
},
],
};
}
// ===== ORDER MANAGEMENT HANDLERS =====
case 'place_order': {
console.error('[Tool] Executing: place_order');
const orderArgs = args as Record<string, unknown>;
const result = await placeOrder({
dhanClientId: orderArgs.dhanClientId as string,
correlationId: orderArgs.correlationId as string,
transactionType: orderArgs.transactionType as 'BUY' | 'SELL',
exchangeSegment: orderArgs.exchangeSegment as string,
productType: orderArgs.productType as 'CNC' | 'INTRADAY' | 'MARGIN' | 'MTF' | 'CO' | 'BO',
orderType: orderArgs.orderType as 'LIMIT' | 'MARKET' | 'STOP_LOSS' | 'STOP_LOSS_MARKET',
validity: orderArgs.validity as 'DAY' | 'IOC',
securityId: orderArgs.securityId as string,
quantity: orderArgs.quantity as number,
price: orderArgs.price as number | undefined,
triggerPrice: orderArgs.triggerPrice as number | undefined,
disclosedQuantity: orderArgs.disclosedQuantity as number | undefined,
afterMarketOrder: orderArgs.afterMarketOrder as boolean | undefined,
amoTime: orderArgs.amoTime as 'PRE_OPEN' | 'OPEN' | 'OPEN_30' | 'OPEN_60' | undefined,
boProfitValue: orderArgs.boProfitValue as number | undefined,
boStopLossValue: orderArgs.boStopLossValue as number | undefined,
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'modify_order': {
console.error('[Tool] Executing: modify_order');
const modifyArgs = args as Record<string, unknown>;
const result = await modifyOrder(modifyArgs.orderId as string, {
dhanClientId: modifyArgs.dhanClientId as string,
orderId: modifyArgs.orderId as string,
orderType: modifyArgs.orderType as 'LIMIT' | 'MARKET' | 'STOP_LOSS' | 'STOP_LOSS_MARKET',
quantity: modifyArgs.quantity as number | undefined,
price: modifyArgs.price as number | undefined,
triggerPrice: modifyArgs.triggerPrice as number | undefined,
disclosedQuantity: modifyArgs.disclosedQuantity as number | undefined,
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'cancel_order': {
console.error('[Tool] Executing: cancel_order');
const { orderId } = args as Record<string, unknown>;
const result = await cancelOrder(orderId as string);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_order_book': {
console.error('[Tool] Executing: get_order_book');
const orders = await getOrderBook();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(orders, null, 2),
},
],
};
}
case 'get_order_by_id': {
console.error('[Tool] Executing: get_order_by_id');
const { orderId } = args as Record<string, unknown>;
const order = await getOrderByID(orderId as string);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(order, null, 2),
},
],
};
}
case 'get_order_by_correlation_id': {
console.error('[Tool] Executing: get_order_by_correlation_id');
const { correlationId } = args as Record<string, unknown>;
const order = await getOrderByCorrelationID(correlationId as string);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(order, null, 2),
},
],
};
}
case 'get_trade_book': {
console.error('[Tool] Executing: get_trade_book');
const trades = await getTradeBook();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(trades, null, 2),
},
],
};
}
case 'get_order_trades': {
console.error('[Tool] Executing: get_order_trades');
const { orderId } = args as Record<string, unknown>;
const trades = await getOrderTrades(orderId as string);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(trades, null, 2),
},
],
};
}
// ===== SUPER ORDER MANAGEMENT HANDLERS =====
case 'place_super_order': {
console.error('[Tool] Executing: place_super_order');
const soArgs = args as Record<string, unknown>;
const result = await placeSuperOrder({
dhanClientId: soArgs.dhanClientId as string,
correlationId: soArgs.correlationId as string,
transactionType: soArgs.transactionType as 'BUY' | 'SELL',
exchangeSegment: soArgs.exchangeSegment as string,
productType: soArgs.productType as 'CNC' | 'INTRADAY' | 'MARGIN' | 'MTF' | 'CO' | 'BO',
orderType: soArgs.orderType as 'LIMIT' | 'MARKET' | 'STOP_LOSS' | 'STOP_LOSS_MARKET',
securityId: soArgs.securityId as string,
quantity: soArgs.quantity as number,
price: soArgs.price as number,
targetPrice: soArgs.targetPrice as number,
stopLossPrice: soArgs.stopLossPrice as number,
trailingJump: soArgs.trailingJump as number | undefined,
});
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'modify_super_order': {
console.error('[Tool] Executing: modify_super_order');
const modSOArgs = args as Record<string, unknown>;
const result = await modifySuperOrder(
modSOArgs.orderId as string,
modSOArgs.legName as string,
modSOArgs
);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'cancel_super_order_leg': {
console.error('[Tool] Executing: cancel_super_order_leg');
const { orderId, orderLeg } = args as Record<string, unknown>;
const result = await cancelSuperOrderLeg(
orderId as string,
orderLeg as string
);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'get_super_order_book': {
console.error('[Tool] Executing: get_super_order_book');
const superOrders = await getSuperOrderBook();
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(superOrders, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[Tool] Execution error: ${errorMessage}`);
throw error;
}
});
// Start server with stdio transport
async function main() {
// Log to stderr to avoid interfering with stdio protocol
const logToStderr = (...args: unknown[]) => {
console.error(...args);
};
logToStderr('🚀 Starting DhanHQ MCP Server...');
logToStderr('📋 Available tools:');
logToStderr(' 🔐 Authentication:');
logToStderr(' • start_authentication - Initiate auth flow (Step 1)');
logToStderr(' • get_login_instructions - Get browser login URL (Step 2)');
logToStderr(' • complete_authentication - Complete auth with tokenId (Step 3)');
logToStderr(' • check_auth_status - Check current auth status');
logToStderr(' • reset_authentication - Reset auth state');
logToStderr(' 💰 Account:');
logToStderr(' • get_fund_limit - Get account balance and margins');
logToStderr(' 📊 Order Management:');
logToStderr(' • place_order - Place a new order');
logToStderr(' • modify_order - Modify pending order');
logToStderr(' • cancel_order - Cancel pending order');
logToStderr(' • get_order_book - Get all orders for the day');
logToStderr(' • get_order_by_id - Get specific order details');
logToStderr(' • get_order_by_correlation_id - Get order by correlation ID');
logToStderr(' • get_trade_book - Get all trades for the day');
logToStderr(' • get_order_trades - Get trades for specific order');
logToStderr(' 🎯 Super Orders (Entry + Target + SL):');
logToStderr(' • place_super_order - Place smart super order');
logToStderr(' • modify_super_order - Modify super order leg');
logToStderr(' • cancel_super_order_leg - Cancel super order leg');
logToStderr(' • get_super_order_book - Get all super orders');
logToStderr('\n💡 To debug, run: npm run inspector\n');
const transport = new StdioServerTransport();
await server.connect(transport);
logToStderr('✓ Server connected and ready for MCP Inspector');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});