sisense-mcp-server — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sisense-mcp-server (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill whenever you're about to add a tool, touch session state, generate artifact IDs, or handle credentials. It captures lessons learned from real bugs in this codebase.
bun run test # unit tests (bun + preloaded setup.ts)
bun run test:e2e # end-to-end tests (requires built server)
bun run build # vite view build + bun SSE server bundle
bun run dev # watch mode (no build needed)
bun run type-check # tsc --noEmit (run before committing)
bun run lint # eslint src/**/*.tsFormat: <type>[(optional scope)]: <description> (SNS-XXXXX)
The Jira ticket (SNS-xxx) is mandatory at the end of the header line.
fix(buildChart): use crypto.randomUUID() for toolCallId (SNS-128032)
Replace requestId with randomUUID so chartIds are unique per tool call.
JSON-RPC requestId is always 1 from Claude, causing every chart to get
the same ID.Supported <type> values:
| Type | When to use |
|---|---|
feat | New feature |
fix | Bug fix |
refactor | Code change that neither fixes a bug nor adds a feature |
style | Whitespace, formatting, semicolons — no logic change |
docs | Documentation only |
build | Build system or dependency changes (scopes: vite, bun) |
ci | CI configuration changes (scope: gitlab) |
perf | Performance improvement |
test | Adding or correcting tests |
chore | Anything that doesn't modify src or test files |
revert | Reverts a previous commit |
Changelog follows common-changelog.org conventions.
HTTP request → sse-server.ts
├── credential hash → persistentStates Map
│ (survives client reconnects for same user)
└── MCP session ID → sessions Map (transport + state ref)
└── setupMcpServer(sessionState) → mcp-server.ts
├── tools/build-chart.ts
├── tools/get-data-sources.ts
└── tools/get-data-source-fields.ts`SessionState = Map<string, unknown>` — the single source of truth per session.
| Key | Type | Persists across MCP re-initialize? |
|---|---|---|
sisenseUrl | string | YES — credential |
sisenseToken | string | YES — credential |
httpClient | HttpClient | YES — credential |
openAIClient | SessionOpenAIClient | YES — credential |
baseUrl | string | REFRESHED — updated from request headers on each re-init (tool-mode screenshot URLs) |
query-${id} | QueryResult | YES — per credential; bounded by MCP_QUERY_CACHE_MAX_ENTRIES (default 10) |
chart:summaries | ChartSummary[] | YES — shared across ChatGPT model/app sessions for same credentials |
chart-${id} | ExtendedChartWidgetProps | YES — key = chartId (same pattern as query cache) |
extra.requestId Is NOT a Unique Tool Call IDThe MCP SDK exposes extra.requestId in tool handlers, but Claude sends requestId=1 for every invocation — it's a JSON-RPC correlation ID, not a per-call unique ID. Never use it to identify artifacts.
Use generateArtifactId() instead:
import { generateArtifactId } from '../utils/string-utils.js';
const toolCallId = generateArtifactId('chart'); // → 'chart-3f2504e0'First 8 hex chars of a UUID — short enough for LLMs to reference, unique enough in practice.
Bug hit in SNS-128031: Tools without credentials were silently falling through to the global Sisense SDK httpClient, causing requests to succeed under the wrong user context.Always get clients from session — never assume a global fallback:
import { getSessionHttpClient, getSessionOpenAIClient } from '../utils/sisense-session.js';
const httpClient = getSessionHttpClient(sessionState); // throws MISSING_SISENSE_SESSION_MESSAGE if absent
const openAIClient = getSessionOpenAIClient(sessionState);These functions throw explicitly. That's intentional — a missing client is a bug, not a fallback case.
_meta Break ClientsBug hit in SNS-128032: Chart payload (serialized widget props + credentials) was returned in the _meta field of the tool response. Cursor and ChatGPT couldn't handle it.>
Follow-up (SNS-129470): Replacing_metawith an MCPResourceTemplate+readServerResource()fixed Cursor but broke ChatGPT — its MCP App host doesn't proxyresources/read. Claude.ai also has a dual-session issue where the Model session stores the payload but the AppBridge session can't find it.
Current pattern — inline `artifactPayload` in `structuredContent`:
buildChart sets artifactPayload: { type: 'chart', sisenseUrl, sisenseToken, serializedWidgetProps } on structuredContent (not in LLM-visible content[0].text).toolResult.structuredContent.artifactPayload from the postMessage toolresult event and renders directly — no HTTP fetch, works across ChatGPT (CSP), Claude dual-session, Goose, and Cursor.Adding a new artifact type (e.g. buildDashboard): set artifactPayload in the tool's structuredContent, add the type to the BiArtifact union in analytics-app.tsx, and handle it in renderArtifact.
Controlled by MCP_APP_ENABLED env var. The feature flag pattern used throughout:
function isMcpAppEnabled(): boolean {
return process.env.MCP_APP_ENABLED !== 'false' && process.env.MCP_APP_ENABLED !== '0';
}Feature flags are enabled by default (opt-out, not opt-in). Check both 'false' and '0'.
| Mode | How chart is delivered | Tool registered via |
|---|---|---|
| App mode (default) | Inline artifactPayload in structuredContent → postMessage | registerAppTool() |
| Tool mode | PNG screenshot, imageUrl in response | server.registerTool() |
When adding a new tool, decide up-front which mode applies and wire it conditionally if needed.
ext-apps ≥ 1.7.2: UnboundedChartWidgetlayout +autoResizecauses infinite loading /host-context-changedspam.
Do: Keep default autoResize: true. Wrap ChartWidget in .chartContainer (width: 100%, tuned min-height) so CSDK has a stable size anchor.
Don't: min-height: 100vh on #root or .main; autoResize: false without sendSizeChanged; gate render on hostContext (gate on !app only).
See src/analytics-view/.
getSessionHttpClient(), getSessionOpenAIClient(), getSessionSisenseUrl(), getSessionSisenseToken() from src/utils/sisense-session.ts. Never touch globals.Date.now(), not extra.requestId, not session counters. Same rule applies to S3 filenames (randomUUID() not Date.now()).csdkBrowserMock.withBrowserEnvironment(async () => { ... }).runWithUserAction('MCP', 'ASSISTANT', () => ...) for telemetry.console.warn(sanitizeError(err).message). Never log raw errors; they may contain tokens in URLs.sanitizeForText(userPrompt) for prompts, sanitizeForDescription() for short strings.buildChart error path).setupMcpServer().bun test preload (src/__test-helpers__/setup.ts mocks browser globals).queryId in build-query.ts; trimmed via trimQueryCache in session-query-cache.ts. Do not clear on MCP re-initialize.ChatGPT opens several MCP HTTP sessions per chat (model tools, MCP app iframe, resources/read). Each sends initialize without mcp-session-id. That is not a new user conversation.
Do not clone/clear persistentStates on re-init — it breaks buildQuery → buildChart (retrieveQuery_miss). Re-init only calls refreshSessionBaseUrl() and reuses the same SessionState Map (sse-server.ts).
Debug with NODE_ENV=development and [mcp-session] logs from devDebug. Expect initialize_reuse with conversationKeys still containing the queryId after buildQuery_stored.
Query cache size: MCP_QUERY_CACHE_MAX_ENTRIES (default 10) — oldest query-* keys evicted on each new buildQuery.
The MCP SDK's JSON Schema validation is bypassed with a no-op validator because AJV has compatibility issues with Bun:
const noOpValidator: any = {
getValidator: () => (input: unknown) => ({ valid: true, data: input, errorMessage: undefined }),
};
new McpServer({ ... }, { jsonSchemaValidator: noOpValidator });Zod handles all validation internally. Don't remove this or try to wire in AJV — it will break.
The persistentStates Map is keyed by a 16-char hex credential hash:
createHash('sha256').update(`${sisenseUrl}:${sisenseToken}`).digest('hex').slice(0, 16);This means: same user with same credentials → same persistent state, regardless of how many times they reconnect. New conversation resets per-conversation keys but preserves credentials and reusable payloads.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.