@optimizeoverseas/lacrm-enforcement-wrapper 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +2 -0
- package/build/index.js +131 -0
- package/package.json +34 -0
package/build/index.d.ts
ADDED
package/build/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
// --- Configuration ---
|
|
8
|
+
const MAX_UNIQUE_CONTACTS = 10;
|
|
9
|
+
const MAX_TOTAL_OPERATIONS = 50;
|
|
10
|
+
const DOWNSTREAM_COMMAND = 'node';
|
|
11
|
+
const DOWNSTREAM_ARGS = ['/usr/local/lib/node_modules/@optimizeoverseas/lacrm-mcp/build/index.js'];
|
|
12
|
+
// Tools whose names start with these prefixes are filtered out entirely
|
|
13
|
+
const BLOCKED_PREFIXES = ['delete_'];
|
|
14
|
+
// Tools that count toward the unique-contacts-modified budget.
|
|
15
|
+
// For create_contact the contact ID comes from the response.
|
|
16
|
+
// For all others, the contact ID comes from the `contact_id` argument.
|
|
17
|
+
const CONTACT_MUTATING_TOOLS = new Set([
|
|
18
|
+
'create_contact',
|
|
19
|
+
'edit_contact',
|
|
20
|
+
]);
|
|
21
|
+
// --- Session state ---
|
|
22
|
+
const modifiedContacts = new Set();
|
|
23
|
+
let totalOperations = 0;
|
|
24
|
+
// --- Helpers ---
|
|
25
|
+
function isBlocked(toolName) {
|
|
26
|
+
return BLOCKED_PREFIXES.some((p) => toolName.startsWith(p));
|
|
27
|
+
}
|
|
28
|
+
function extractContactId(toolName, args) {
|
|
29
|
+
if (toolName === 'edit_contact') {
|
|
30
|
+
return args.contact_id;
|
|
31
|
+
}
|
|
32
|
+
// create_contact: ID comes from response, handled after forwarding
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function checkBudgets(toolName, args) {
|
|
36
|
+
if (totalOperations >= MAX_TOTAL_OPERATIONS) {
|
|
37
|
+
return `Session operation limit reached (${MAX_TOTAL_OPERATIONS}). No more operations can be performed in this session.`;
|
|
38
|
+
}
|
|
39
|
+
if (CONTACT_MUTATING_TOOLS.has(toolName)) {
|
|
40
|
+
const contactId = extractContactId(toolName, args);
|
|
41
|
+
// For edit_contact, check if adding this contact would exceed the limit
|
|
42
|
+
if (contactId && !modifiedContacts.has(contactId) && modifiedContacts.size >= MAX_UNIQUE_CONTACTS) {
|
|
43
|
+
return `Unique contact modification limit reached (${MAX_UNIQUE_CONTACTS}). You have already modified ${MAX_UNIQUE_CONTACTS} distinct contacts in this session. You may still edit contacts you have already modified: ${[...modifiedContacts].join(', ')}`;
|
|
44
|
+
}
|
|
45
|
+
// For create_contact (no contactId yet), check if there's room for one more
|
|
46
|
+
if (!contactId && modifiedContacts.size >= MAX_UNIQUE_CONTACTS) {
|
|
47
|
+
return `Unique contact modification limit reached (${MAX_UNIQUE_CONTACTS}). Cannot create new contacts — you have already modified ${MAX_UNIQUE_CONTACTS} distinct contacts in this session.`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function recordOperation(toolName, args, result) {
|
|
53
|
+
totalOperations++;
|
|
54
|
+
if (!CONTACT_MUTATING_TOOLS.has(toolName))
|
|
55
|
+
return;
|
|
56
|
+
// edit_contact: ID from args
|
|
57
|
+
const contactId = extractContactId(toolName, args);
|
|
58
|
+
if (contactId) {
|
|
59
|
+
modifiedContacts.add(contactId);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// create_contact: try to extract ContactId from response text
|
|
63
|
+
if (toolName === 'create_contact' && result && typeof result === 'object') {
|
|
64
|
+
const content = result.content;
|
|
65
|
+
if (content?.[0]?.text) {
|
|
66
|
+
const match = content[0].text.match(/ContactId:\s*(\S+)/);
|
|
67
|
+
if (match) {
|
|
68
|
+
modifiedContacts.add(match[1]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// --- Main ---
|
|
74
|
+
async function main() {
|
|
75
|
+
// 1. Connect to downstream LACRM MCP as a client
|
|
76
|
+
const downstreamTransport = new StdioClientTransport({
|
|
77
|
+
command: DOWNSTREAM_COMMAND,
|
|
78
|
+
args: DOWNSTREAM_ARGS,
|
|
79
|
+
env: { ...process.env },
|
|
80
|
+
});
|
|
81
|
+
const downstream = new Client({ name: 'lacrm-enforcement-wrapper', version: '1.0.0' }, { capabilities: {} });
|
|
82
|
+
await downstream.connect(downstreamTransport);
|
|
83
|
+
// 2. Create upstream MCP server
|
|
84
|
+
const server = new Server({ name: 'lacrm-enforcement-wrapper', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
85
|
+
// 3. Handle tools/list — proxy from downstream, filter delete_*
|
|
86
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
87
|
+
const response = await downstream.listTools();
|
|
88
|
+
const filtered = response.tools.filter((t) => !isBlocked(t.name));
|
|
89
|
+
return { tools: filtered };
|
|
90
|
+
});
|
|
91
|
+
// 4. Handle tools/call — enforce policies, then forward
|
|
92
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
93
|
+
const { name, arguments: args } = request.params;
|
|
94
|
+
// Block delete tools that somehow slip through
|
|
95
|
+
if (isBlocked(name)) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: `Operation "${name}" is not permitted. Delete operations are blocked.` }],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Check session budgets
|
|
102
|
+
const budgetError = checkBudgets(name, (args ?? {}));
|
|
103
|
+
if (budgetError) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: budgetError }],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Forward to downstream
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = await downstream.callTool({ name, arguments: args });
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text', text: `Downstream error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
117
|
+
isError: true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Record the operation for budget tracking
|
|
121
|
+
recordOperation(name, (args ?? {}), result);
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
// 5. Connect server to stdio (facing Allegiance AI)
|
|
125
|
+
const upstreamTransport = new StdioServerTransport();
|
|
126
|
+
await server.connect(upstreamTransport);
|
|
127
|
+
}
|
|
128
|
+
main().catch((err) => {
|
|
129
|
+
console.error('Wrapper MCP server failed to start:', err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optimizeoverseas/lacrm-enforcement-wrapper",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP proxy wrapper enforcing session limits on LACRM MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"lacrm-enforcement-wrapper": "build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepare": "npm run build",
|
|
16
|
+
"start": "node build/index.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.22.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22",
|
|
23
|
+
"typescript": "^5.3.3"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/optimize-overseas/lacrmenforcement-wrapper.git"
|
|
28
|
+
},
|
|
29
|
+
"author": "Optimize Overseas",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|