@loopops/mcp-server 3.47.0 → 3.49.0
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/dist/api-client.js +8 -1
- package/dist/tools/_html-ui.d.ts +60 -0
- package/dist/tools/_html-ui.js +205 -0
- package/dist/tools/reporting.js +23 -7
- package/package.json +2 -1
package/dist/api-client.js
CHANGED
|
@@ -32,7 +32,14 @@ let refreshToken = process.env.OKTA_REFRESH_TOKEN;
|
|
|
32
32
|
// Access token cache (in-memory, subprocess-lifetime).
|
|
33
33
|
let cachedAccessToken = null;
|
|
34
34
|
let cachedAccessTokenExpiresAt = 0; // epoch ms
|
|
35
|
-
|
|
35
|
+
// 120s default. The fast tools (cpq_context, find_opportunity, my_quotes,
|
|
36
|
+
// account_show, etc.) return in <2s; the slow tools (create_prep_brief,
|
|
37
|
+
// draft_quote_from_intent) chain web search + Claude composition and can
|
|
38
|
+
// legitimately take 30-90s. The old 30s default tripped users on the slow
|
|
39
|
+
// tools when their MCP client config didn't override. Override via
|
|
40
|
+
// API_TIMEOUT_MS env (Claude Code CLI sets 300000 from `loop-ops login`;
|
|
41
|
+
// Desktop config should mirror that).
|
|
42
|
+
const DEFAULT_TIMEOUT_MS = Number(process.env.API_TIMEOUT_MS || 120_000);
|
|
36
43
|
const SERVER_NAME = "loop-operations";
|
|
37
44
|
// Refresh a little early so requests don't race token expiry at the edge.
|
|
38
45
|
const ACCESS_TOKEN_EARLY_REFRESH_MS = 60 * 1000; // 1 minute
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP UI extension support — render tool output as styled HTML so Claude
|
|
3
|
+
* Desktop renders it in a side panel instead of inline.
|
|
4
|
+
*
|
|
5
|
+
* Claude Desktop's `initialize` handshake declares support for the
|
|
6
|
+
* io.modelcontextprotocol/ui extension with `text/html;profile=mcp-app`.
|
|
7
|
+
* When a tool returns an `embedded_resource` with that mime type, Desktop
|
|
8
|
+
* renders it in a side panel with native copy/print/share affordances —
|
|
9
|
+
* the "create a document" UX the team wants for prep briefs and similar
|
|
10
|
+
* long-form artifacts.
|
|
11
|
+
*
|
|
12
|
+
* Clients without the UI extension (Claude Code CLI, Codex) just see the
|
|
13
|
+
* markdown text content block — same content, inline.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* server.tool(name, desc, schema, safeToolUi(async args => {
|
|
17
|
+
* const md = await trpcMutation(...);
|
|
18
|
+
* return {
|
|
19
|
+
* markdown: md,
|
|
20
|
+
* title: "Customer Prep — Acme",
|
|
21
|
+
* };
|
|
22
|
+
* }))
|
|
23
|
+
*/
|
|
24
|
+
type ContentBlock = {
|
|
25
|
+
type: "text";
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
type: "resource";
|
|
29
|
+
resource: {
|
|
30
|
+
uri: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
text: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
type McpToolResult = {
|
|
36
|
+
content: ContentBlock[];
|
|
37
|
+
isError?: boolean;
|
|
38
|
+
};
|
|
39
|
+
interface UiResponse {
|
|
40
|
+
/** Markdown body — always returned as a text content block. */
|
|
41
|
+
markdown: string;
|
|
42
|
+
/** Title shown at the top of the HTML doc + tab. */
|
|
43
|
+
title: string;
|
|
44
|
+
/** Optional meta line under the title (e.g., "generated at … · account …"). */
|
|
45
|
+
meta?: string;
|
|
46
|
+
/** Optional uri identifier. Defaults to a synthesized one. */
|
|
47
|
+
uri?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Tool wrapper that returns content as BOTH markdown (inline fallback)
|
|
51
|
+
* AND HTML (Desktop side-panel render). Mirrors safeTool's error handling.
|
|
52
|
+
*
|
|
53
|
+
* Use this for long-form artifacts where the user wants a document
|
|
54
|
+
* experience: prep briefs, quote previews, deal-health snapshots.
|
|
55
|
+
*
|
|
56
|
+
* Short tool outputs (status messages, counts, single-fact lookups)
|
|
57
|
+
* should stick with plain safeTool — adding HTML would just be noise.
|
|
58
|
+
*/
|
|
59
|
+
export declare function safeToolUi<Args>(fn: (args: Args) => Promise<UiResponse>): (args: Args) => Promise<McpToolResult>;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP UI extension support — render tool output as styled HTML so Claude
|
|
3
|
+
* Desktop renders it in a side panel instead of inline.
|
|
4
|
+
*
|
|
5
|
+
* Claude Desktop's `initialize` handshake declares support for the
|
|
6
|
+
* io.modelcontextprotocol/ui extension with `text/html;profile=mcp-app`.
|
|
7
|
+
* When a tool returns an `embedded_resource` with that mime type, Desktop
|
|
8
|
+
* renders it in a side panel with native copy/print/share affordances —
|
|
9
|
+
* the "create a document" UX the team wants for prep briefs and similar
|
|
10
|
+
* long-form artifacts.
|
|
11
|
+
*
|
|
12
|
+
* Clients without the UI extension (Claude Code CLI, Codex) just see the
|
|
13
|
+
* markdown text content block — same content, inline.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* server.tool(name, desc, schema, safeToolUi(async args => {
|
|
17
|
+
* const md = await trpcMutation(...);
|
|
18
|
+
* return {
|
|
19
|
+
* markdown: md,
|
|
20
|
+
* title: "Customer Prep — Acme",
|
|
21
|
+
* };
|
|
22
|
+
* }))
|
|
23
|
+
*/
|
|
24
|
+
import { marked } from "marked";
|
|
25
|
+
import { ApiAuthError, ApiHttpError, ApiNetworkError, ApiTimeoutError, } from "../api-client.js";
|
|
26
|
+
const MCP_APP_MIME = "text/html;profile=mcp-app";
|
|
27
|
+
/**
|
|
28
|
+
* Convert a markdown brief / quote / report into a styled, self-contained
|
|
29
|
+
* HTML document for Desktop's side-panel render.
|
|
30
|
+
*
|
|
31
|
+
* Self-contained means no external assets — all styles inlined. Lets the
|
|
32
|
+
* client cache + share the doc without hot-linking.
|
|
33
|
+
*/
|
|
34
|
+
function renderMcpAppHtml(opts) {
|
|
35
|
+
const body = marked.parse(opts.markdown, { gfm: true, breaks: false });
|
|
36
|
+
const safeTitle = escapeHtml(opts.title);
|
|
37
|
+
const safeMeta = opts.meta ? escapeHtml(opts.meta) : "";
|
|
38
|
+
return `<!DOCTYPE html>
|
|
39
|
+
<html lang="en">
|
|
40
|
+
<head>
|
|
41
|
+
<meta charset="utf-8">
|
|
42
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
43
|
+
<title>${safeTitle}</title>
|
|
44
|
+
<style>
|
|
45
|
+
:root {
|
|
46
|
+
--bg: #ffffff;
|
|
47
|
+
--fg: #0f172a;
|
|
48
|
+
--muted: #64748b;
|
|
49
|
+
--line: #e2e8f0;
|
|
50
|
+
--code-bg: #f1f5f9;
|
|
51
|
+
--pre-bg: #f8fafc;
|
|
52
|
+
--accent: #0ea5e9;
|
|
53
|
+
--blockquote: #475569;
|
|
54
|
+
}
|
|
55
|
+
@media (prefers-color-scheme: dark) {
|
|
56
|
+
:root {
|
|
57
|
+
--bg: #0f172a;
|
|
58
|
+
--fg: #f1f5f9;
|
|
59
|
+
--muted: #94a3b8;
|
|
60
|
+
--line: #1e293b;
|
|
61
|
+
--code-bg: #1e293b;
|
|
62
|
+
--pre-bg: #0b1224;
|
|
63
|
+
--accent: #38bdf8;
|
|
64
|
+
--blockquote: #cbd5e1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
* { box-sizing: border-box; }
|
|
68
|
+
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); }
|
|
69
|
+
body {
|
|
70
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", system-ui, sans-serif;
|
|
71
|
+
font-size: 15px;
|
|
72
|
+
line-height: 1.6;
|
|
73
|
+
max-width: 760px;
|
|
74
|
+
margin: 0 auto;
|
|
75
|
+
padding: 40px 32px 80px;
|
|
76
|
+
}
|
|
77
|
+
h1 { font-size: 26px; margin: 0 0 8px; line-height: 1.25; }
|
|
78
|
+
h2 { font-size: 20px; margin: 32px 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
|
|
79
|
+
h3 { font-size: 16px; margin: 24px 0 8px; }
|
|
80
|
+
p { margin: 12px 0; }
|
|
81
|
+
ul, ol { padding-left: 24px; }
|
|
82
|
+
li { margin: 4px 0; }
|
|
83
|
+
blockquote {
|
|
84
|
+
margin: 16px 0;
|
|
85
|
+
padding: 8px 16px;
|
|
86
|
+
border-left: 3px solid var(--accent);
|
|
87
|
+
color: var(--blockquote);
|
|
88
|
+
background: var(--code-bg);
|
|
89
|
+
border-radius: 0 4px 4px 0;
|
|
90
|
+
}
|
|
91
|
+
code {
|
|
92
|
+
background: var(--code-bg);
|
|
93
|
+
padding: 2px 6px;
|
|
94
|
+
border-radius: 4px;
|
|
95
|
+
font-family: "SF Mono", Menlo, monospace;
|
|
96
|
+
font-size: 0.92em;
|
|
97
|
+
}
|
|
98
|
+
pre {
|
|
99
|
+
background: var(--pre-bg);
|
|
100
|
+
padding: 16px;
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
overflow-x: auto;
|
|
103
|
+
border: 1px solid var(--line);
|
|
104
|
+
}
|
|
105
|
+
pre code { background: transparent; padding: 0; }
|
|
106
|
+
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
|
|
107
|
+
th, td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; vertical-align: top; }
|
|
108
|
+
th { background: var(--code-bg); font-weight: 600; }
|
|
109
|
+
hr { border: 0; border-top: 1px solid var(--line); margin: 32px 0; }
|
|
110
|
+
a { color: var(--accent); text-decoration: none; }
|
|
111
|
+
a:hover { text-decoration: underline; }
|
|
112
|
+
.header { border-bottom: 1px solid var(--line); padding-bottom: 16px; margin-bottom: 24px; }
|
|
113
|
+
.meta { color: var(--muted); font-size: 13px; margin-top: 4px; }
|
|
114
|
+
@media print {
|
|
115
|
+
body { padding: 0; max-width: none; }
|
|
116
|
+
.no-print { display: none; }
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<div class="header">
|
|
122
|
+
<h1>${safeTitle}</h1>
|
|
123
|
+
${safeMeta ? `<div class="meta">${safeMeta}</div>` : ""}
|
|
124
|
+
</div>
|
|
125
|
+
<div class="content">
|
|
126
|
+
${body}
|
|
127
|
+
</div>
|
|
128
|
+
</body>
|
|
129
|
+
</html>`;
|
|
130
|
+
}
|
|
131
|
+
function escapeHtml(s) {
|
|
132
|
+
return s
|
|
133
|
+
.replace(/&/g, "&")
|
|
134
|
+
.replace(/</g, "<")
|
|
135
|
+
.replace(/>/g, ">")
|
|
136
|
+
.replace(/"/g, """)
|
|
137
|
+
.replace(/'/g, "'");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Tool wrapper that returns content as BOTH markdown (inline fallback)
|
|
141
|
+
* AND HTML (Desktop side-panel render). Mirrors safeTool's error handling.
|
|
142
|
+
*
|
|
143
|
+
* Use this for long-form artifacts where the user wants a document
|
|
144
|
+
* experience: prep briefs, quote previews, deal-health snapshots.
|
|
145
|
+
*
|
|
146
|
+
* Short tool outputs (status messages, counts, single-fact lookups)
|
|
147
|
+
* should stick with plain safeTool — adding HTML would just be noise.
|
|
148
|
+
*/
|
|
149
|
+
export function safeToolUi(fn) {
|
|
150
|
+
return async (args) => {
|
|
151
|
+
try {
|
|
152
|
+
const result = await fn(args);
|
|
153
|
+
const html = renderMcpAppHtml(result);
|
|
154
|
+
const uri = result.uri ?? `loop://artifacts/${slugify(result.title)}`;
|
|
155
|
+
return {
|
|
156
|
+
content: [
|
|
157
|
+
{ type: "text", text: result.markdown },
|
|
158
|
+
{
|
|
159
|
+
type: "resource",
|
|
160
|
+
resource: {
|
|
161
|
+
uri,
|
|
162
|
+
mimeType: MCP_APP_MIME,
|
|
163
|
+
text: html,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
const text = formatErrorForUser(err);
|
|
171
|
+
console.error("[MCP] Tool error:", err);
|
|
172
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function slugify(s) {
|
|
177
|
+
return s
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
180
|
+
.replace(/^-|-$/g, "")
|
|
181
|
+
.slice(0, 80);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reused from _helpers — keep one canonical error formatter. Slight
|
|
185
|
+
* duplication is fine; not worth a cross-file import for 20 lines.
|
|
186
|
+
*/
|
|
187
|
+
function formatErrorForUser(err) {
|
|
188
|
+
if (err instanceof ApiAuthError) {
|
|
189
|
+
if (err.serverMessage)
|
|
190
|
+
return err.serverMessage;
|
|
191
|
+
return ("Your Loop access couldn't be verified. " +
|
|
192
|
+
"Re-mint your MCP key at https://www.loopops.io/mcp/connect (Okta sign-in required), " +
|
|
193
|
+
"then restart Claude Desktop. If the problem persists, contact Revenue Operations.");
|
|
194
|
+
}
|
|
195
|
+
if (err instanceof ApiTimeoutError) {
|
|
196
|
+
return `The Loop API timed out after ${err.timeoutMs}ms. Slow tools (create_prep_brief, draft_quote_from_intent) can take 30-90s; if you keep hitting this, ask Revenue Operations to raise API_TIMEOUT_MS in your MCP client config.`;
|
|
197
|
+
}
|
|
198
|
+
if (err instanceof ApiHttpError) {
|
|
199
|
+
return `Loop API returned HTTP ${err.status}: ${err.body.slice(0, 300) || "(no body)"}.`;
|
|
200
|
+
}
|
|
201
|
+
if (err instanceof ApiNetworkError) {
|
|
202
|
+
return `Couldn't reach the Loop API: ${err.message}.`;
|
|
203
|
+
}
|
|
204
|
+
return `Tool failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
205
|
+
}
|
package/dist/tools/reporting.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { trpcMutation, trpcQuery } from "../api-client.js";
|
|
3
3
|
import { safeTool } from "./_helpers.js";
|
|
4
|
+
import { safeToolUi } from "./_html-ui.js";
|
|
4
5
|
import { rangeSchema, salesforceLeadIdSchema, } from "./_schemas.js";
|
|
5
6
|
export function registerReportingTools(server, allowed) {
|
|
6
7
|
// loop_health retired 2026-05-06 — replaced by ClickHouse parameterized
|
|
@@ -50,12 +51,27 @@ export function registerReportingTools(server, allowed) {
|
|
|
50
51
|
.enum(["concise", "standard", "detailed"])
|
|
51
52
|
.optional()
|
|
52
53
|
.describe("Output verbosity. concise ~400 words; standard ~900 (default); detailed ~1500."),
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
}, safeToolUi(async ({ accountDomain, conversationType, meetingContext, localContent, style }) => {
|
|
55
|
+
const markdown = await trpcMutation("mcp.createPrepBrief", {
|
|
56
|
+
accountDomain,
|
|
57
|
+
conversationType,
|
|
58
|
+
meetingContext,
|
|
59
|
+
localContent,
|
|
60
|
+
style: style ?? "standard",
|
|
61
|
+
});
|
|
62
|
+
// Pretty title for the document tab + heading.
|
|
63
|
+
const conversationLabel = conversationType
|
|
64
|
+
.split("_")
|
|
65
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
66
|
+
.join(" ");
|
|
67
|
+
return {
|
|
68
|
+
markdown,
|
|
69
|
+
title: `${accountDomain} — ${conversationLabel} prep`,
|
|
70
|
+
meta: meetingContext
|
|
71
|
+
? `Meeting context: ${meetingContext}`
|
|
72
|
+
: undefined,
|
|
73
|
+
uri: `loop://briefs/${accountDomain}/${conversationType}/${Date.now()}`,
|
|
74
|
+
};
|
|
75
|
+
}));
|
|
60
76
|
}
|
|
61
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopops/mcp-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.49.0",
|
|
4
4
|
"description": "Loop Operations MCP Server — AI skills for RevOps",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
31
|
+
"marked": "^18.0.4",
|
|
31
32
|
"yaml": "^2.6.0",
|
|
32
33
|
"zod": "^3.24.4"
|
|
33
34
|
},
|