-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathserver.mts
More file actions
276 lines (252 loc) · 9.97 KB
/
Copy pathserver.mts
File metadata and controls
276 lines (252 loc) · 9.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
import { Server } from '@modelcontextprotocol/server'
import type {
McpRequestContext,
ServerContext,
} from '@modelcontextprotocol/server'
import { getSocketApiUrl } from './env.mts'
import { defineAlertsTool } from './tool-alerts.mts'
import { defineDepscoreTool } from './tool-depscore.mts'
import {
definePackageFileContentsTool,
definePackageFileGrepTool,
definePackageFilesTool,
} from './tool-package-files.mts'
import { defineOrganizationsTool } from './tool-organizations.mts'
import { defineThreatFeedTool } from './tool-threat-feed.mts'
import { withToolLogging } from './tool-logging.mts'
import type { ToolHandler } from './tool-logging.mts'
import type {
ToolAnnotations,
ToolHandlerExtra,
ToolInputSchema,
ToolSpec,
} from './tool-types.mts'
import { buildMcpUserAgent } from './user-agent.mts'
import { VERSION } from './version.mts'
// One entry in the `tools/list` payload — the wire shape clients read.
export interface ToolListEntry {
annotations?: ToolAnnotations | undefined
description: string
inputSchema: ToolInputSchema
name: string
title: string
}
export interface ToolErrorResult {
[key: string]: unknown
content: Array<{ type: 'text'; text: string }>
isError: true
}
export interface ToolOkResult {
[key: string]: unknown
content: Array<{ type: 'text'; text: string }>
}
// Base URL for the org-scoped Socket REST API (alerts, organizations,
// threat-feed, file-list). Shared by every tool module so the fallback lives
// in one place.
export const SOCKET_API_BASE_URL = getSocketApiurl() || 'https://api.socket.dev'
// The single auth-missing message every tool returns when no token is
// available — kept here so the wording stays identical across tools.
export const AUTH_REQUIRED_MSG =
'Authentication is required. Set SOCKET_API_TOKEN for stdio mode, or send your Socket API token as an `Authorization: Bearer <token>` header (or connect through OAuth) in HTTP mode.'
// Boot-time static API key. In stdio mode this is the local user's own token
// (set from SOCKET_API_TOKEN in index.mts), so it is safe to use for any tool.
// In HTTP mode it is the deploy operator's token, shared across every caller —
// `staticApiKeyShared` records that distinction so per-tenant tools never hand
// the operator's private data to an arbitrary caller.
let staticApiKey: string = ''
let staticApiKeyShared = false
// `tools/list` is a cacheable result on the 2026-07-28 revision: the SDK
// stamps `ttlMs`/`cacheScope` onto the wire response from this hint. The tool
// set only changes when a new socket-mcp ships and is identical for every
// caller, so a one-hour shared-cache lifetime is safe.
const TOOLS_LIST_CACHE_TTL_MS = 60 * 60 * 1000
// Shared "auth missing" tool result — every tool returns the same shape so
// clients get a consistent error.
export function authRequiredResult(): ToolErrorResult {
return errorResult(AUTH_REQUIRED_MSG)
}
/**
* Build the canonical set of tool specs. Each tool ships its own
* `define*Tool()` factory so the data + handler stay co-located; this function
* just collects them and hands each schema through `toPlainSchemaSpec`. Order
* here is the order clients see in `tools/list`.
*/
export function buildToolSpecs(): ToolSpec[] {
return [
defineDepscoreTool(),
defineOrganizationsTool(),
defineAlertsTool(),
defineThreatFeedTool(),
definePackageFilesTool(),
definePackageFileContentsTool(),
definePackageFileGrepTool(),
].map(toPlainSchemaSpec)
}
/**
* Build a configured low-level `Server` instance with every Socket tool
* registered. Both serving entries take this as their factory and own the
* lifetime of what it returns — `serveStdio` closes its discarded
* `server/discover` probe instance and `createMcpHandler` closes the instance
* it built after each HTTP exchange — so every call must hand back a fresh
* `Server`.
*
* The low-level `Server` accepts raw JSON Schema in `Tool.inputSchema`, which
* is exactly what TypeBox's `Type.Object({...})` produces, and `tools/list`
* results are not re-parsed on the way out. So every tool's input schema flows
* through the SDK to clients verbatim; no zod, no extra validation layer here.
*/
export function createConfiguredServer(
requestContext?: McpRequestContext | undefined,
): Server {
const specs = buildToolSpecs()
const userAgent = getMcpRequestUserAgent(requestContext)
const handlers = new Map<string, ToolHandler>(
specs.map(spec => [
spec.name,
withToolLogging(spec.name, spec.handler.bind(spec)),
]),
)
const server = new Server(
{ name: 'socket', version: VERSION },
{
capabilities: { tools: {} },
cacheHints: {
'tools/list': {
ttlMs: TOOLS_LIST_CACHE_TTL_MS,
cacheScope: 'public',
},
},
},
)
server.setRequestHandler('tools/list', () => ({
__proto__: null,
tools: specs.map(toToolListEntry),
}))
server.setRequestHandler('tools/call', async (request, ctx) => {
const { name } = request.params
const handler = handlers.get(name)
if (!handler) {
// The CallTool spec returns an error result (not an exception) for an
// unknown name — clients render it the same way as any other tool error.
const message = `Unknown tool: ${name}`
return {
__proto__: null,
content: [{ type: 'text', text: message }],
isError: true,
}
}
// `request.params.arguments` is optional in the SDK shape; tools that
// declare empty inputSchemas (e.g. organizations) get undefined here.
const args = request.params.arguments ?? {}
return handler(args, toToolHandlerExtra(ctx, userAgent))
})
return server
}
export function errorResult(text: string): ToolErrorResult {
return {
__proto__: null,
content: [{ type: 'text', text }],
isError: true,
}
}
// Adapt the SDK's per-request handler context to the local `ToolHandlerExtra`
// shape the tool modules read. `ctx.http` is only populated on an HTTP
// transport, so stdio callers get the MCP user agent without auth info and
// fall back to the boot-time static key inside the tool body.
export function getMcpRequestUserAgent(
ctx?: McpRequestContext | undefined,
): string {
return buildMcpUserAgent(
ctx?.requestInfo?.headers.get('user-agent') ?? undefined,
)
}
export function getStaticApiKey(): string {
return staticApiKey
}
// Resolve the access token a PUBLIC-data tool (depscore) should use: the
// per-request token takes precedence, then the boot-time static key. Falling
// back to a shared deploy key is fine here because package scores are not
// tenant-scoped. Returns undefined when neither is available.
export function resolveAuthToken(
authInfoToken: string | undefined,
): string | undefined {
return authInfoToken || staticApiKey || undefined
}
// Resolve the access token a PER-TENANT tool (organizations, alerts,
// threat_feed, package_files) should use. The per-request token always wins.
// The static key is only an acceptable fallback when it is the local user's
// own token (stdio mode); in HTTP mode the static key belongs to the deploy
// operator, so returning it would expose the operator's private org data to
// every caller. Returns undefined in that case so the tool emits
// AUTH_REQUIRED instead of silently acting as the operator.
export function resolveScopedAuthToken(
authInfoToken: string | undefined,
): string | undefined {
if (authInfoToken) {
return authInfoToken
}
if (!staticApiKeyShared && staticApiKey) {
return staticApiKey
}
return undefined
}
// Set the static API key. Called once during boot from index.mts. `shared`
// marks the key as a deploy-operator key (HTTP mode) rather than the local
// user's own (stdio mode). Subsequent calls overwrite — only the most recent
// value is used.
export function setStaticApiKey(
value: string,
options?: { shared?: boolean | undefined } | undefined,
): void {
const opts = { __proto__: null, ...options } as {
shared?: boolean | undefined
}
staticApiKey = value
staticApiKeyShared = opts.shared ?? false
}
/**
* Return the spec with its `inputSchema` deep-copied into a plain JSON-Schema
* object. TypeBox's `Type.*` constructors hang symbol-keyed metadata —
* `Symbol(TypeBox.Kind)`, `Symbol(TypeBox.Optional)` — off every schema node
* they build, and a symbol key is not JSON Schema. Serializing transports
* (stdio, Streamable HTTP) drop those symbols on the way out, but a transport
* that hands objects over by reference passes them straight to the consumer,
* where a result validator rejects the payload. Copying here makes a spec
* structurally what it claims to be: raw JSON Schema, safe for any consumer.
*
* The JSON round trip is the copy: every schema node holds only strings,
* numbers, booleans, arrays and plain objects — no `undefined` values, no
* `Date`/`Map`/`Set` — so the result is byte-identical to what a serializing
* transport already writes.
*/
export function toPlainSchemaSpec(spec: ToolSpec): ToolSpec {
// JSON.parse widens to any; the value round-trips through the same
// encoding every shipping transport applies to it.
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- round trip
const inputSchema = JSON.parse(
JSON.stringify(spec.inputSchema),
) as ToolInputSchema
return { ...spec, inputSchema }
}
export function toToolHandlerExtra(
ctx: ServerContext,
userAgent = buildMcpUserAgent(),
): ToolHandlerExtra {
const authInfo = ctx.http?.authInfo
return { ...(authInfo ? { authInfo } : {}), userAgent }
}
/**
* Render one spec as its `tools/list` entry. `annotations` is optional on
* `ToolSpec`, so a tool that declares none is published without the key rather
* than with an empty object — clients treat a present-but-empty `annotations`
* as a set of explicit hints.
*/
export function toToolListEntry(spec: ToolSpec): ToolListEntry {
return {
name: spec.name,
title: spec.title,
description: spec.description,
inputSchema: spec.inputSchema,
...(spec.annotations ? { annotations: spec.annotations } : {}),
}
}