@company-semantics/contracts 0.85.0 → 0.87.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/package.json +2 -2
- package/src/api/http/routes/ai-chat.ts +37 -0
- package/src/index.ts +15 -3
- package/src/interfaces/mcp/tools/help.ts +76 -0
- package/src/mcp/capability-graph.test.ts +421 -0
- package/src/mcp/capability-graph.ts +113 -0
- package/src/mcp/index.ts +182 -7
- package/src/mcp/resources.ts +21 -0
- package/src/org/index.ts +0 -4
- package/src/org/capabilities.ts +0 -84
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"guard:test": "vitest run scripts/ci/__tests__",
|
|
78
78
|
"release": "npx tsx scripts/release.ts",
|
|
79
79
|
"prepublishOnly": "echo 'ERROR: Publishing is CI-only via tag push. Use pnpm release instead.' && exit 1",
|
|
80
|
-
"test": "vitest run
|
|
80
|
+
"test": "vitest run"
|
|
81
81
|
},
|
|
82
82
|
"packageManager": "pnpm@10.25.0",
|
|
83
83
|
"engines": {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Chat Route — Tool Discovery API Contract
|
|
3
|
+
*
|
|
4
|
+
* Shared helper for building tool discovery responses with optional
|
|
5
|
+
* capability graph inclusion via ?include=graph query parameter.
|
|
6
|
+
*
|
|
7
|
+
* Backend implementation: company-semantics-backend/src/api/http/routes/capabilities.ts
|
|
8
|
+
*
|
|
9
|
+
* Consumer usage:
|
|
10
|
+
* import { buildCapabilityGraph } from '@company-semantics/contracts'
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { buildCapabilityGraph } from '../../../mcp/capability-graph'
|
|
14
|
+
import type {
|
|
15
|
+
MCPToolDescriptor,
|
|
16
|
+
ToolDiscoveryResponse,
|
|
17
|
+
CapabilityGraph,
|
|
18
|
+
} from '../../../mcp/index'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a ToolDiscoveryResponse, optionally including the capability graph.
|
|
22
|
+
*
|
|
23
|
+
* When include=graph query parameter is present, the response includes
|
|
24
|
+
* the graph derived from tool resource flow metadata.
|
|
25
|
+
*
|
|
26
|
+
* @param tools - Tool descriptors to include in response
|
|
27
|
+
* @param includeGraph - Whether to include the capability graph (from ?include=graph)
|
|
28
|
+
*/
|
|
29
|
+
export function buildToolDiscoveryResponse(
|
|
30
|
+
tools: MCPToolDescriptor[],
|
|
31
|
+
includeGraph: boolean,
|
|
32
|
+
): ToolDiscoveryResponse {
|
|
33
|
+
return {
|
|
34
|
+
tools,
|
|
35
|
+
...(includeGraph && { graph: buildCapabilityGraph(tools) }),
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -224,8 +224,6 @@ export type {
|
|
|
224
224
|
OrgAuthPolicy,
|
|
225
225
|
UpdateAuthPolicyRequest,
|
|
226
226
|
Phase3AuditAction,
|
|
227
|
-
// Workspace capability types (Phase 3)
|
|
228
|
-
WorkspaceCapability,
|
|
229
227
|
// Domain and multi-org types (Phase 4)
|
|
230
228
|
// @see ADR-CONT-032 for design rationale
|
|
231
229
|
DomainStatus,
|
|
@@ -260,7 +258,7 @@ export type {
|
|
|
260
258
|
StrategyDoc,
|
|
261
259
|
} from './org/index'
|
|
262
260
|
|
|
263
|
-
export { ROLE_DISPLAY_MAP,
|
|
261
|
+
export { ROLE_DISPLAY_MAP, VIEW_SCOPE_MAP, getViewScope, TRANSFER_RESPONSIBILITIES, IDENTITY_TRUST_LEVEL_LABELS } from './org/index'
|
|
264
262
|
|
|
265
263
|
// View authorization types (Phase 5 - ADR-APP-013)
|
|
266
264
|
export type { AuthorizableView } from './org/index'
|
|
@@ -276,8 +274,22 @@ export type {
|
|
|
276
274
|
ToolDiscoveryResponse,
|
|
277
275
|
ToolListMessagePart,
|
|
278
276
|
ToolListDataPart,
|
|
277
|
+
// Tool domain taxonomy (PRD-00265)
|
|
278
|
+
ToolDomain,
|
|
279
|
+
ToolIntegration,
|
|
280
|
+
ToolIntent,
|
|
281
|
+
ToolStability,
|
|
282
|
+
ToolComplexity,
|
|
283
|
+
// Resource flow types (PRD-00265)
|
|
284
|
+
ResourceType,
|
|
285
|
+
// Capability graph types (PRD-00265 types, PRD-00268 implementation)
|
|
286
|
+
CapabilityGraph,
|
|
287
|
+
CapabilityGraphEdge,
|
|
288
|
+
ToolWorkflow,
|
|
279
289
|
} from './mcp/index'
|
|
280
290
|
|
|
291
|
+
export { buildCapabilityGraph } from './mcp/index'
|
|
292
|
+
|
|
281
293
|
// Message part types and builder functions
|
|
282
294
|
// @see ADR-2026-01-022 for design rationale
|
|
283
295
|
export type {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help Tool Enrichment — Workflow Summaries & Domain Groupings
|
|
3
|
+
*
|
|
4
|
+
* Pure formatting functions for enriching cs_help output with
|
|
5
|
+
* workflow summaries and domain groupings derived from the
|
|
6
|
+
* capability graph.
|
|
7
|
+
*
|
|
8
|
+
* These functions accept MCPToolDescriptor[] (the shape returned
|
|
9
|
+
* by getToolHandlers() or getToolDefinitions() in the backend)
|
|
10
|
+
* and produce formatted text sections.
|
|
11
|
+
*
|
|
12
|
+
* @see company-semantics-backend/src/interfaces/mcp/tools/system/help.ts
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { MCPToolDescriptor } from '../../../mcp/index'
|
|
16
|
+
import { buildCapabilityGraph } from '../../../mcp/capability-graph'
|
|
17
|
+
|
|
18
|
+
function capitalize(s: string): string {
|
|
19
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build workflow summary text from tool descriptors.
|
|
24
|
+
*
|
|
25
|
+
* Derives workflows via buildCapabilityGraph() and formats them
|
|
26
|
+
* for the "Available workflows" section of cs_help output.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* const descriptors = getToolDefinitions() // or from getToolHandlers()
|
|
30
|
+
* const section = formatWorkflowSummaries(descriptors)
|
|
31
|
+
*/
|
|
32
|
+
export function formatWorkflowSummaries(
|
|
33
|
+
descriptors: MCPToolDescriptor[],
|
|
34
|
+
): string {
|
|
35
|
+
const graph = buildCapabilityGraph(descriptors)
|
|
36
|
+
if (graph.workflows.length === 0) return ''
|
|
37
|
+
|
|
38
|
+
const lines = graph.workflows.map((w) => {
|
|
39
|
+
const displayName = capitalize(w.name.replace(/_/g, ' '))
|
|
40
|
+
const steps = w.steps.map((s) => s.replace(/^cs_/, '')).join(' → ')
|
|
41
|
+
return ` ${displayName}: ${steps}`
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return '\nAvailable workflows:\n' + lines.join('\n')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build domain grouping text from tool descriptors.
|
|
49
|
+
*
|
|
50
|
+
* Groups tools by their `domain` field and formats as the
|
|
51
|
+
* "Tool domains" section of cs_help output.
|
|
52
|
+
*
|
|
53
|
+
* Usage:
|
|
54
|
+
* const descriptors = getToolDefinitions() // or from getToolHandlers()
|
|
55
|
+
* const section = formatDomainGroupings(descriptors)
|
|
56
|
+
*/
|
|
57
|
+
export function formatDomainGroupings(
|
|
58
|
+
descriptors: MCPToolDescriptor[],
|
|
59
|
+
): string {
|
|
60
|
+
const domainGroups = new Map<string, string[]>()
|
|
61
|
+
|
|
62
|
+
for (const tool of descriptors) {
|
|
63
|
+
const domain = tool.domain ?? 'unknown'
|
|
64
|
+
if (!domainGroups.has(domain)) domainGroups.set(domain, [])
|
|
65
|
+
domainGroups.get(domain)!.push(
|
|
66
|
+
tool.name.replace(/^cs_/, '').replace(/_/g, ' '),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lines = [...domainGroups.entries()].map(
|
|
71
|
+
([domain, tools]) =>
|
|
72
|
+
` ${capitalize(domain)} (${tools.length}): ${tools.join(', ')}`,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return '\nTool domains:\n' + lines.join('\n')
|
|
76
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildCapabilityGraph } from './capability-graph'
|
|
3
|
+
import type { MCPToolDescriptor } from './index'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper to create minimal MCPToolDescriptor for testing.
|
|
7
|
+
* Only id, name, and resource fields matter for graph derivation.
|
|
8
|
+
*/
|
|
9
|
+
function makeTool(
|
|
10
|
+
overrides: Partial<MCPToolDescriptor> & { id: string; name: string },
|
|
11
|
+
): MCPToolDescriptor {
|
|
12
|
+
return {
|
|
13
|
+
category: 'system',
|
|
14
|
+
description: '',
|
|
15
|
+
effectClass: 'pure',
|
|
16
|
+
invocationMode: 'manual',
|
|
17
|
+
visibility: 'user',
|
|
18
|
+
requiresConfirmation: false,
|
|
19
|
+
domain: 'system',
|
|
20
|
+
risk: 'none',
|
|
21
|
+
intent: 'read',
|
|
22
|
+
stability: 'stable',
|
|
23
|
+
complexity: 'trivial',
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
...overrides,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('buildCapabilityGraph', () => {
|
|
30
|
+
it('returns empty graph for empty tools array', () => {
|
|
31
|
+
const graph = buildCapabilityGraph([])
|
|
32
|
+
expect(graph.edges).toEqual([])
|
|
33
|
+
expect(graph.workflows).toEqual([])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('produces no edges for tools with no produces/consumes', () => {
|
|
37
|
+
const tools = [
|
|
38
|
+
makeTool({ id: 'a', name: 'a' }),
|
|
39
|
+
makeTool({ id: 'b', name: 'b' }),
|
|
40
|
+
]
|
|
41
|
+
const graph = buildCapabilityGraph(tools)
|
|
42
|
+
expect(graph.edges).toEqual([])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('creates one edge for single producer → consumer with correct domain', () => {
|
|
46
|
+
const tools = [
|
|
47
|
+
makeTool({ id: 'a', name: 'a', produces: ['integration.connection'] }),
|
|
48
|
+
makeTool({ id: 'b', name: 'b', consumes: ['integration.connection'] }),
|
|
49
|
+
]
|
|
50
|
+
const graph = buildCapabilityGraph(tools)
|
|
51
|
+
expect(graph.edges).toHaveLength(1)
|
|
52
|
+
expect(graph.edges[0]).toEqual({
|
|
53
|
+
from: 'a',
|
|
54
|
+
to: 'b',
|
|
55
|
+
resource: 'integration.connection',
|
|
56
|
+
domain: 'integration',
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('creates multiple edges for multiple producers of same resource', () => {
|
|
61
|
+
const tools = [
|
|
62
|
+
makeTool({ id: 'a', name: 'a', produces: ['integration.connection'] }),
|
|
63
|
+
makeTool({ id: 'b', name: 'b', produces: ['integration.connection'] }),
|
|
64
|
+
makeTool({ id: 'c', name: 'c', consumes: ['integration.connection'] }),
|
|
65
|
+
]
|
|
66
|
+
const graph = buildCapabilityGraph(tools)
|
|
67
|
+
expect(graph.edges).toHaveLength(2)
|
|
68
|
+
expect(graph.edges).toContainEqual({
|
|
69
|
+
from: 'a',
|
|
70
|
+
to: 'c',
|
|
71
|
+
resource: 'integration.connection',
|
|
72
|
+
domain: 'integration',
|
|
73
|
+
})
|
|
74
|
+
expect(graph.edges).toContainEqual({
|
|
75
|
+
from: 'b',
|
|
76
|
+
to: 'c',
|
|
77
|
+
resource: 'integration.connection',
|
|
78
|
+
domain: 'integration',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('derives workflow for linear chain > 2 steps', () => {
|
|
83
|
+
const tools = [
|
|
84
|
+
makeTool({
|
|
85
|
+
id: 'step1',
|
|
86
|
+
name: 'step1',
|
|
87
|
+
produces: ['integration.connection'],
|
|
88
|
+
}),
|
|
89
|
+
makeTool({
|
|
90
|
+
id: 'step2',
|
|
91
|
+
name: 'step2',
|
|
92
|
+
consumes: ['integration.connection'],
|
|
93
|
+
produces: ['slack.channel'],
|
|
94
|
+
}),
|
|
95
|
+
makeTool({
|
|
96
|
+
id: 'step3',
|
|
97
|
+
name: 'step3',
|
|
98
|
+
consumes: ['slack.channel'],
|
|
99
|
+
produces: ['slack.channel_scope'],
|
|
100
|
+
}),
|
|
101
|
+
makeTool({
|
|
102
|
+
id: 'step4',
|
|
103
|
+
name: 'step4',
|
|
104
|
+
consumes: ['slack.channel_scope'],
|
|
105
|
+
}),
|
|
106
|
+
]
|
|
107
|
+
const graph = buildCapabilityGraph(tools)
|
|
108
|
+
expect(graph.workflows).toHaveLength(1)
|
|
109
|
+
expect(graph.workflows[0].steps).toEqual([
|
|
110
|
+
'step1',
|
|
111
|
+
'step2',
|
|
112
|
+
'step3',
|
|
113
|
+
'step4',
|
|
114
|
+
])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does NOT derive workflow for short chain (2 steps, 1 edge)', () => {
|
|
118
|
+
const tools = [
|
|
119
|
+
makeTool({ id: 'a', name: 'a', produces: ['integration.connection'] }),
|
|
120
|
+
makeTool({ id: 'b', name: 'b', consumes: ['integration.connection'] }),
|
|
121
|
+
]
|
|
122
|
+
const graph = buildCapabilityGraph(tools)
|
|
123
|
+
expect(graph.workflows).toEqual([])
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('does not form single workflow from branching nodes', () => {
|
|
127
|
+
const tools = [
|
|
128
|
+
makeTool({
|
|
129
|
+
id: 'root',
|
|
130
|
+
name: 'root',
|
|
131
|
+
produces: ['integration.connection'],
|
|
132
|
+
}),
|
|
133
|
+
makeTool({
|
|
134
|
+
id: 'branch1',
|
|
135
|
+
name: 'branch1',
|
|
136
|
+
consumes: ['integration.connection'],
|
|
137
|
+
produces: ['slack.channel'],
|
|
138
|
+
}),
|
|
139
|
+
makeTool({
|
|
140
|
+
id: 'branch2',
|
|
141
|
+
name: 'branch2',
|
|
142
|
+
consumes: ['integration.connection'],
|
|
143
|
+
produces: ['slack.coverage'],
|
|
144
|
+
}),
|
|
145
|
+
]
|
|
146
|
+
const graph = buildCapabilityGraph(tools)
|
|
147
|
+
// root has 2 outgoing edges → not a linear chain
|
|
148
|
+
expect(graph.workflows).toEqual([])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('full 17-tool integration test', () => {
|
|
152
|
+
const allTools: MCPToolDescriptor[] = [
|
|
153
|
+
// Organization
|
|
154
|
+
makeTool({
|
|
155
|
+
id: 'cs_get_org_status',
|
|
156
|
+
name: 'cs_get_org_status',
|
|
157
|
+
domain: 'organization',
|
|
158
|
+
produces: ['org.status'],
|
|
159
|
+
}),
|
|
160
|
+
makeTool({
|
|
161
|
+
id: 'cs_update_org',
|
|
162
|
+
name: 'cs_update_org',
|
|
163
|
+
domain: 'organization',
|
|
164
|
+
intent: 'mutate',
|
|
165
|
+
}),
|
|
166
|
+
// Identity
|
|
167
|
+
makeTool({
|
|
168
|
+
id: 'cs_update_profile',
|
|
169
|
+
name: 'cs_update_profile',
|
|
170
|
+
domain: 'identity',
|
|
171
|
+
intent: 'mutate',
|
|
172
|
+
}),
|
|
173
|
+
// Integrations
|
|
174
|
+
makeTool({
|
|
175
|
+
id: 'cs_start_slack_auth',
|
|
176
|
+
name: 'cs_start_slack_auth',
|
|
177
|
+
domain: 'integrations',
|
|
178
|
+
integrations: ['slack'],
|
|
179
|
+
produces: ['integration.connection'],
|
|
180
|
+
}),
|
|
181
|
+
makeTool({
|
|
182
|
+
id: 'cs_start_google_auth',
|
|
183
|
+
name: 'cs_start_google_auth',
|
|
184
|
+
domain: 'integrations',
|
|
185
|
+
integrations: ['google'],
|
|
186
|
+
produces: ['integration.connection'],
|
|
187
|
+
}),
|
|
188
|
+
makeTool({
|
|
189
|
+
id: 'cs_start_zoom_auth',
|
|
190
|
+
name: 'cs_start_zoom_auth',
|
|
191
|
+
domain: 'integrations',
|
|
192
|
+
integrations: ['zoom'],
|
|
193
|
+
produces: ['integration.connection'],
|
|
194
|
+
}),
|
|
195
|
+
makeTool({
|
|
196
|
+
id: 'cs_list_connections',
|
|
197
|
+
name: 'cs_list_connections',
|
|
198
|
+
domain: 'integrations',
|
|
199
|
+
produces: ['integration.connection'],
|
|
200
|
+
}),
|
|
201
|
+
makeTool({
|
|
202
|
+
id: 'cs_cleanup_connections',
|
|
203
|
+
name: 'cs_cleanup_connections',
|
|
204
|
+
domain: 'integrations',
|
|
205
|
+
consumes: ['integration.connection'],
|
|
206
|
+
}),
|
|
207
|
+
makeTool({
|
|
208
|
+
id: 'cs_propose_integration_action',
|
|
209
|
+
name: 'cs_propose_integration_action',
|
|
210
|
+
domain: 'integrations',
|
|
211
|
+
consumes: ['integration.connection'],
|
|
212
|
+
}),
|
|
213
|
+
// Discovery
|
|
214
|
+
makeTool({
|
|
215
|
+
id: 'cs_discover_slack',
|
|
216
|
+
name: 'cs_discover_slack',
|
|
217
|
+
domain: 'discovery',
|
|
218
|
+
integrations: ['slack'],
|
|
219
|
+
produces: ['slack.channel'],
|
|
220
|
+
consumes: ['integration.connection'],
|
|
221
|
+
}),
|
|
222
|
+
makeTool({
|
|
223
|
+
id: 'cs_get_slack_coverage',
|
|
224
|
+
name: 'cs_get_slack_coverage',
|
|
225
|
+
domain: 'discovery',
|
|
226
|
+
integrations: ['slack'],
|
|
227
|
+
produces: ['slack.coverage'],
|
|
228
|
+
consumes: ['integration.connection'],
|
|
229
|
+
}),
|
|
230
|
+
makeTool({
|
|
231
|
+
id: 'cs_list_fingerprints',
|
|
232
|
+
name: 'cs_list_fingerprints',
|
|
233
|
+
domain: 'discovery',
|
|
234
|
+
produces: ['knowledge.fingerprint'],
|
|
235
|
+
}),
|
|
236
|
+
// Ingestion
|
|
237
|
+
makeTool({
|
|
238
|
+
id: 'cs_manage_channel_scope',
|
|
239
|
+
name: 'cs_manage_channel_scope',
|
|
240
|
+
domain: 'ingestion',
|
|
241
|
+
integrations: ['slack'],
|
|
242
|
+
produces: ['slack.channel_scope'],
|
|
243
|
+
consumes: ['slack.channel'],
|
|
244
|
+
}),
|
|
245
|
+
makeTool({
|
|
246
|
+
id: 'cs_ingest_slack_channel',
|
|
247
|
+
name: 'cs_ingest_slack_channel',
|
|
248
|
+
domain: 'ingestion',
|
|
249
|
+
integrations: ['slack'],
|
|
250
|
+
produces: ['ingestion.job'],
|
|
251
|
+
consumes: ['slack.channel_scope'],
|
|
252
|
+
}),
|
|
253
|
+
makeTool({
|
|
254
|
+
id: 'cs_get_ingestion_status',
|
|
255
|
+
name: 'cs_get_ingestion_status',
|
|
256
|
+
domain: 'ingestion',
|
|
257
|
+
produces: ['ingestion.job'],
|
|
258
|
+
consumes: ['ingestion.job'],
|
|
259
|
+
}),
|
|
260
|
+
// System
|
|
261
|
+
makeTool({
|
|
262
|
+
id: 'cs_system_status',
|
|
263
|
+
name: 'cs_system_status',
|
|
264
|
+
domain: 'system',
|
|
265
|
+
produces: ['system.status'],
|
|
266
|
+
}),
|
|
267
|
+
makeTool({
|
|
268
|
+
id: 'cs_help',
|
|
269
|
+
name: 'cs_help',
|
|
270
|
+
domain: 'system',
|
|
271
|
+
}),
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
it('includes Slack ingestion chain edges', () => {
|
|
275
|
+
const graph = buildCapabilityGraph(allTools)
|
|
276
|
+
|
|
277
|
+
expect(graph.edges).toContainEqual({
|
|
278
|
+
from: 'cs_start_slack_auth',
|
|
279
|
+
to: 'cs_discover_slack',
|
|
280
|
+
resource: 'integration.connection',
|
|
281
|
+
domain: 'integration',
|
|
282
|
+
})
|
|
283
|
+
expect(graph.edges).toContainEqual({
|
|
284
|
+
from: 'cs_discover_slack',
|
|
285
|
+
to: 'cs_manage_channel_scope',
|
|
286
|
+
resource: 'slack.channel',
|
|
287
|
+
domain: 'slack',
|
|
288
|
+
})
|
|
289
|
+
expect(graph.edges).toContainEqual({
|
|
290
|
+
from: 'cs_manage_channel_scope',
|
|
291
|
+
to: 'cs_ingest_slack_channel',
|
|
292
|
+
resource: 'slack.channel_scope',
|
|
293
|
+
domain: 'slack',
|
|
294
|
+
})
|
|
295
|
+
expect(graph.edges).toContainEqual({
|
|
296
|
+
from: 'cs_ingest_slack_channel',
|
|
297
|
+
to: 'cs_get_ingestion_status',
|
|
298
|
+
resource: 'ingestion.job',
|
|
299
|
+
domain: 'ingestion',
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('produces correct total edge count', () => {
|
|
304
|
+
const graph = buildCapabilityGraph(allTools)
|
|
305
|
+
// integration.connection: 4 producers × 4 consumers = 16 edges
|
|
306
|
+
// slack.channel: 1 edge (discover_slack → manage_channel_scope)
|
|
307
|
+
// slack.channel_scope: 1 edge (manage_channel_scope → ingest_slack_channel)
|
|
308
|
+
// ingestion.job: 2 edges (ingest → status, status → status self-loop)
|
|
309
|
+
expect(graph.edges).toHaveLength(20)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('does not derive workflows from branching graph', () => {
|
|
313
|
+
const graph = buildCapabilityGraph(allTools)
|
|
314
|
+
// With all 17 tools, auth tools have 4 outgoing edges each (branching),
|
|
315
|
+
// and consumers like discover_slack have 4 incoming edges.
|
|
316
|
+
// No linear chain starts are walkable → no workflows.
|
|
317
|
+
expect(graph.workflows).toEqual([])
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
describe('slack_ingestion workflow derivation', () => {
|
|
322
|
+
it('derives slack_ingestion workflow from Slack chain tools', () => {
|
|
323
|
+
// Isolated Slack chain without branching from other auth tools
|
|
324
|
+
const slackChainTools = [
|
|
325
|
+
makeTool({
|
|
326
|
+
id: 'cs_start_slack_auth',
|
|
327
|
+
name: 'cs_start_slack_auth',
|
|
328
|
+
integrations: ['slack'],
|
|
329
|
+
produces: ['integration.connection'],
|
|
330
|
+
}),
|
|
331
|
+
makeTool({
|
|
332
|
+
id: 'cs_discover_slack',
|
|
333
|
+
name: 'cs_discover_slack',
|
|
334
|
+
integrations: ['slack'],
|
|
335
|
+
produces: ['slack.channel'],
|
|
336
|
+
consumes: ['integration.connection'],
|
|
337
|
+
}),
|
|
338
|
+
makeTool({
|
|
339
|
+
id: 'cs_manage_channel_scope',
|
|
340
|
+
name: 'cs_manage_channel_scope',
|
|
341
|
+
integrations: ['slack'],
|
|
342
|
+
produces: ['slack.channel_scope'],
|
|
343
|
+
consumes: ['slack.channel'],
|
|
344
|
+
}),
|
|
345
|
+
makeTool({
|
|
346
|
+
id: 'cs_ingest_slack_channel',
|
|
347
|
+
name: 'cs_ingest_slack_channel',
|
|
348
|
+
integrations: ['slack'],
|
|
349
|
+
consumes: ['slack.channel_scope'],
|
|
350
|
+
}),
|
|
351
|
+
]
|
|
352
|
+
const graph = buildCapabilityGraph(slackChainTools)
|
|
353
|
+
expect(graph.workflows).toHaveLength(1)
|
|
354
|
+
expect(graph.workflows[0].name).toBe('slack_ingestion')
|
|
355
|
+
expect(graph.workflows[0].steps).toEqual([
|
|
356
|
+
'cs_start_slack_auth',
|
|
357
|
+
'cs_discover_slack',
|
|
358
|
+
'cs_manage_channel_scope',
|
|
359
|
+
'cs_ingest_slack_channel',
|
|
360
|
+
])
|
|
361
|
+
expect(graph.workflows[0].description).toContain('start_slack_auth')
|
|
362
|
+
expect(graph.workflows[0].description).toContain('ingest_slack_channel')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('extends Slack chain through cs_get_ingestion_status when isolated', () => {
|
|
366
|
+
const slackChainWithStatus = [
|
|
367
|
+
makeTool({
|
|
368
|
+
id: 'cs_start_slack_auth',
|
|
369
|
+
name: 'cs_start_slack_auth',
|
|
370
|
+
integrations: ['slack'],
|
|
371
|
+
produces: ['integration.connection'],
|
|
372
|
+
}),
|
|
373
|
+
makeTool({
|
|
374
|
+
id: 'cs_discover_slack',
|
|
375
|
+
name: 'cs_discover_slack',
|
|
376
|
+
integrations: ['slack'],
|
|
377
|
+
produces: ['slack.channel'],
|
|
378
|
+
consumes: ['integration.connection'],
|
|
379
|
+
}),
|
|
380
|
+
makeTool({
|
|
381
|
+
id: 'cs_manage_channel_scope',
|
|
382
|
+
name: 'cs_manage_channel_scope',
|
|
383
|
+
integrations: ['slack'],
|
|
384
|
+
produces: ['slack.channel_scope'],
|
|
385
|
+
consumes: ['slack.channel'],
|
|
386
|
+
}),
|
|
387
|
+
makeTool({
|
|
388
|
+
id: 'cs_ingest_slack_channel',
|
|
389
|
+
name: 'cs_ingest_slack_channel',
|
|
390
|
+
integrations: ['slack'],
|
|
391
|
+
produces: ['ingestion.job'],
|
|
392
|
+
consumes: ['slack.channel_scope'],
|
|
393
|
+
}),
|
|
394
|
+
makeTool({
|
|
395
|
+
id: 'cs_get_ingestion_status',
|
|
396
|
+
name: 'cs_get_ingestion_status',
|
|
397
|
+
produces: ['ingestion.job'],
|
|
398
|
+
consumes: ['ingestion.job'],
|
|
399
|
+
}),
|
|
400
|
+
]
|
|
401
|
+
const graph = buildCapabilityGraph(slackChainWithStatus)
|
|
402
|
+
|
|
403
|
+
expect(graph.workflows).toHaveLength(1)
|
|
404
|
+
// Chain stops at cs_ingest_slack_channel because cs_get_ingestion_status
|
|
405
|
+
// has 2 incoming edges (from ingest + self-loop)
|
|
406
|
+
expect(graph.workflows[0].steps).toContain('cs_start_slack_auth')
|
|
407
|
+
expect(graph.workflows[0].steps).toContain('cs_discover_slack')
|
|
408
|
+
expect(graph.workflows[0].steps).toContain('cs_manage_channel_scope')
|
|
409
|
+
expect(graph.workflows[0].steps).toContain('cs_ingest_slack_channel')
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('extracts domain from resource namespace prefix', () => {
|
|
414
|
+
const tools = [
|
|
415
|
+
makeTool({ id: 'a', name: 'a', produces: ['slack.channel'] }),
|
|
416
|
+
makeTool({ id: 'b', name: 'b', consumes: ['slack.channel'] }),
|
|
417
|
+
]
|
|
418
|
+
const graph = buildCapabilityGraph(tools)
|
|
419
|
+
expect(graph.edges[0].domain).toBe('slack')
|
|
420
|
+
})
|
|
421
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MCPToolDescriptor,
|
|
3
|
+
CapabilityGraph,
|
|
4
|
+
CapabilityGraphEdge,
|
|
5
|
+
ToolWorkflow,
|
|
6
|
+
} from './index'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a capability graph from tool resource flow metadata.
|
|
10
|
+
*
|
|
11
|
+
* Pure function — no I/O, no database, no side effects.
|
|
12
|
+
* Derives edges from produces/consumes matching on MCPToolDescriptor.
|
|
13
|
+
*
|
|
14
|
+
* For each tool's `consumes` array, finds all tools whose `produces`
|
|
15
|
+
* includes that ResourceType and creates a CapabilityGraphEdge.
|
|
16
|
+
*/
|
|
17
|
+
export function buildCapabilityGraph(tools: MCPToolDescriptor[]): CapabilityGraph {
|
|
18
|
+
const edges: CapabilityGraphEdge[] = []
|
|
19
|
+
|
|
20
|
+
for (const consumer of tools) {
|
|
21
|
+
for (const resourceType of consumer.consumes ?? []) {
|
|
22
|
+
const producers = tools.filter(t => t.produces?.includes(resourceType))
|
|
23
|
+
for (const producer of producers) {
|
|
24
|
+
edges.push({
|
|
25
|
+
from: producer.id,
|
|
26
|
+
to: consumer.id,
|
|
27
|
+
resource: resourceType,
|
|
28
|
+
domain: resourceType.split('.')[0],
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const workflows = deriveWorkflows(tools, edges)
|
|
35
|
+
return { edges, workflows }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derive named workflows from capability graph edges.
|
|
40
|
+
*
|
|
41
|
+
* Conservative: only linear chains with > 2 steps are reported.
|
|
42
|
+
* Branching nodes (multiple incoming or outgoing edges) break chains.
|
|
43
|
+
*
|
|
44
|
+
* Internal to capability-graph.ts — not exported.
|
|
45
|
+
*/
|
|
46
|
+
function deriveWorkflows(
|
|
47
|
+
tools: MCPToolDescriptor[],
|
|
48
|
+
edges: CapabilityGraphEdge[],
|
|
49
|
+
): ToolWorkflow[] {
|
|
50
|
+
// Build adjacency: tool ID → outgoing edges, tool ID → incoming edges
|
|
51
|
+
const outgoing = new Map<string, CapabilityGraphEdge[]>()
|
|
52
|
+
const incoming = new Map<string, CapabilityGraphEdge[]>()
|
|
53
|
+
|
|
54
|
+
for (const edge of edges) {
|
|
55
|
+
if (!outgoing.has(edge.from)) outgoing.set(edge.from, [])
|
|
56
|
+
outgoing.get(edge.from)!.push(edge)
|
|
57
|
+
if (!incoming.has(edge.to)) incoming.set(edge.to, [])
|
|
58
|
+
incoming.get(edge.to)!.push(edge)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Find chain start nodes: have outgoing edges, no incoming edges
|
|
62
|
+
const startNodes = [...outgoing.keys()].filter(
|
|
63
|
+
id => !incoming.has(id) || incoming.get(id)!.length === 0,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const workflows: ToolWorkflow[] = []
|
|
67
|
+
const visited = new Set<string>()
|
|
68
|
+
|
|
69
|
+
for (const start of startNodes) {
|
|
70
|
+
if (visited.has(start)) continue
|
|
71
|
+
|
|
72
|
+
const chain: string[] = [start]
|
|
73
|
+
visited.add(start)
|
|
74
|
+
let current = start
|
|
75
|
+
|
|
76
|
+
// Walk forward: follow single outgoing edge while next node has single incoming edge
|
|
77
|
+
while (true) {
|
|
78
|
+
const outs = outgoing.get(current)
|
|
79
|
+
if (!outs || outs.length !== 1) break
|
|
80
|
+
|
|
81
|
+
const next = outs[0].to
|
|
82
|
+
const ins = incoming.get(next)
|
|
83
|
+
if (!ins || ins.length !== 1) break
|
|
84
|
+
|
|
85
|
+
if (visited.has(next)) break
|
|
86
|
+
visited.add(next)
|
|
87
|
+
chain.push(next)
|
|
88
|
+
current = next
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Only emit workflows with > 2 steps
|
|
92
|
+
if (chain.length > 2) {
|
|
93
|
+
const toolMap = new Map(tools.map(t => [t.id, t]))
|
|
94
|
+
const chainTools = chain.map(id => toolMap.get(id)).filter(Boolean)
|
|
95
|
+
|
|
96
|
+
// Derive name from common integration or domain
|
|
97
|
+
const integrations = chainTools
|
|
98
|
+
.flatMap(t => t!.integrations ?? [])
|
|
99
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
100
|
+
const name =
|
|
101
|
+
integrations.length === 1
|
|
102
|
+
? `${integrations[0]}_ingestion`
|
|
103
|
+
: `workflow_${chain[0]}`
|
|
104
|
+
|
|
105
|
+
const steps = chain.map(s => s.replace(/^cs_/, ''))
|
|
106
|
+
const description = `Workflow: ${steps.join(' → ')}`
|
|
107
|
+
|
|
108
|
+
workflows.push({ name: name, description: description, steps: chain })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return workflows
|
|
113
|
+
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { ResourceType } from './resources'
|
|
2
|
+
export type { ResourceType } from './resources'
|
|
3
|
+
export { buildCapabilityGraph } from './capability-graph'
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* MCP Tool Discovery Types
|
|
3
7
|
*
|
|
@@ -18,15 +22,38 @@
|
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* @deprecated Use ToolDomain instead. ToolCategory is too coarse —
|
|
26
|
+
* 'data' conflates discovery, ingestion, and coverage.
|
|
23
27
|
*/
|
|
24
28
|
export type ToolCategory =
|
|
25
|
-
| 'system'
|
|
26
|
-
| 'data'
|
|
27
|
-
| 'connections'
|
|
28
|
-
| 'automation'
|
|
29
|
-
| 'developer'
|
|
29
|
+
| 'system'
|
|
30
|
+
| 'data'
|
|
31
|
+
| 'connections'
|
|
32
|
+
| 'automation'
|
|
33
|
+
| 'developer'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tool domain taxonomy.
|
|
37
|
+
*
|
|
38
|
+
* Each value maps to a distinct domain boundary in the system architecture.
|
|
39
|
+
* Replaces ToolCategory which was too coarse for resource flow reasoning.
|
|
40
|
+
*
|
|
41
|
+
* - organization: Org settings, membership, billing queries
|
|
42
|
+
* - identity: User profile, auth state, preferences
|
|
43
|
+
* - integrations: OAuth connections, provider management
|
|
44
|
+
* - ingestion: Data import, sync jobs, source configuration
|
|
45
|
+
* - discovery: Search, exploration, content queries
|
|
46
|
+
* - knowledge: Fingerprints, embeddings, knowledge graph
|
|
47
|
+
* - system: Health, status, runtime diagnostics
|
|
48
|
+
*/
|
|
49
|
+
export type ToolDomain =
|
|
50
|
+
| 'organization'
|
|
51
|
+
| 'identity'
|
|
52
|
+
| 'integrations'
|
|
53
|
+
| 'ingestion'
|
|
54
|
+
| 'discovery'
|
|
55
|
+
| 'knowledge'
|
|
56
|
+
| 'system'
|
|
30
57
|
|
|
31
58
|
/**
|
|
32
59
|
* Tool visibility levels.
|
|
@@ -53,12 +80,58 @@ export type ToolInvocationMode = 'manual' | 'assistant' | 'hybrid'
|
|
|
53
80
|
*/
|
|
54
81
|
export type ToolEffectClass = 'pure' | 'effectful'
|
|
55
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Tool intent classification.
|
|
85
|
+
*
|
|
86
|
+
* Classifies what the tool does to the system:
|
|
87
|
+
* - read: Pure data retrieval (no side effects)
|
|
88
|
+
* - mutate: State change (creates, updates, deletes)
|
|
89
|
+
* - analysis: Computation over data without mutation (aggregation, scoring)
|
|
90
|
+
*
|
|
91
|
+
* Note: 'control' was removed — system-level operations (health, status,
|
|
92
|
+
* runtime) use 'read' intent in the 'system' domain. Control is a domain
|
|
93
|
+
* classification, not an intent.
|
|
94
|
+
*
|
|
95
|
+
* INVARIANT: intent 'mutate' + risk 'high' → requiresConfirmation must be true.
|
|
96
|
+
* Enforced in backend tool registration, typed here for documentation.
|
|
97
|
+
*/
|
|
98
|
+
export type ToolIntent = 'read' | 'mutate' | 'analysis'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Tool lifecycle stability classification.
|
|
102
|
+
*
|
|
103
|
+
* - experimental: May change or be removed without notice
|
|
104
|
+
* - stable: Production-ready with backward compatibility guarantees
|
|
105
|
+
* - deprecated: Scheduled for removal, consumers should migrate
|
|
106
|
+
*/
|
|
107
|
+
export type ToolStability = 'experimental' | 'stable' | 'deprecated'
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tool computational complexity classification.
|
|
111
|
+
*
|
|
112
|
+
* Classifies the cost and latency profile of a tool invocation:
|
|
113
|
+
* - trivial: Sub-100ms, in-memory or single DB lookup
|
|
114
|
+
* - moderate: 100ms-5s, may involve external API calls
|
|
115
|
+
* - heavy: 5s+, batch processing, large data transfers, long-running ops
|
|
116
|
+
*/
|
|
117
|
+
export type ToolComplexity = 'trivial' | 'moderate' | 'heavy'
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Integration provider tag for tools.
|
|
121
|
+
*
|
|
122
|
+
* Known values correspond to registered OAuth providers.
|
|
123
|
+
* Extensible via (string & {}) — new integrations do not require
|
|
124
|
+
* a contracts release, but known values get autocomplete.
|
|
125
|
+
*/
|
|
126
|
+
export type ToolIntegration = 'slack' | 'google' | 'zoom' | (string & {})
|
|
127
|
+
|
|
56
128
|
/**
|
|
57
129
|
* Complete tool descriptor for discovery and invocation.
|
|
58
130
|
*
|
|
59
131
|
* Discovery uses: id, name, description, category
|
|
60
132
|
* Invocation uses: id, requiresConfirmation, invocationMode, effectClass
|
|
61
133
|
*/
|
|
134
|
+
// @vocabulary-exempt reason: MCPToolDescriptor is a single cohesive descriptor consumed by multiple repos; splitting would break consumers
|
|
62
135
|
export interface MCPToolDescriptor {
|
|
63
136
|
/** Unique identifier (matches MCP tool name, e.g., 'cs_help') */
|
|
64
137
|
id: string
|
|
@@ -85,6 +158,101 @@ export interface MCPToolDescriptor {
|
|
|
85
158
|
invocationMode: ToolInvocationMode
|
|
86
159
|
/** Who can see this tool */
|
|
87
160
|
visibility: ToolVisibility
|
|
161
|
+
|
|
162
|
+
// --- New fields (PRD-00265) ---
|
|
163
|
+
|
|
164
|
+
/** Domain taxonomy classification. Replaces category for routing and grouping. */
|
|
165
|
+
domain: ToolDomain
|
|
166
|
+
/**
|
|
167
|
+
* Integration providers this tool interacts with.
|
|
168
|
+
* Empty or omitted for tools that don't touch external integrations.
|
|
169
|
+
*/
|
|
170
|
+
integrations?: ToolIntegration[]
|
|
171
|
+
/**
|
|
172
|
+
* User-impact risk classification.
|
|
173
|
+
* - none: Read-only, no side effects
|
|
174
|
+
* - low: Reversible mutation (can be undone)
|
|
175
|
+
* - moderate: User-visible side effect (notification sent, state changed)
|
|
176
|
+
* - high: Destructive or irreversible (data deletion, external action)
|
|
177
|
+
*
|
|
178
|
+
* INVARIANT: intent 'mutate' + risk 'high' → requiresConfirmation must be true.
|
|
179
|
+
*/
|
|
180
|
+
risk: 'none' | 'low' | 'moderate' | 'high'
|
|
181
|
+
/** What the tool does to the system (read, mutate, analysis). */
|
|
182
|
+
intent: ToolIntent
|
|
183
|
+
/** Lifecycle stability classification. */
|
|
184
|
+
stability: ToolStability
|
|
185
|
+
/** Computational complexity and latency profile. */
|
|
186
|
+
complexity: ToolComplexity
|
|
187
|
+
/**
|
|
188
|
+
* Input schema version number.
|
|
189
|
+
* Disambiguates input schema versioning from tool behavior versioning.
|
|
190
|
+
* Increment when the tool's input schema changes shape.
|
|
191
|
+
*/
|
|
192
|
+
schemaVersion: number
|
|
193
|
+
/**
|
|
194
|
+
* Authorization scopes required to invoke this tool.
|
|
195
|
+
* References scope strings from trust/scopes.ts.
|
|
196
|
+
* Omit for tools with no specific scope requirements.
|
|
197
|
+
*/
|
|
198
|
+
scopes?: string[]
|
|
199
|
+
/**
|
|
200
|
+
* Resource types this tool produces (creates or outputs).
|
|
201
|
+
* Used for capability graph derivation — enables workflow ordering
|
|
202
|
+
* via resource flow instead of explicit `requires` fields.
|
|
203
|
+
*/
|
|
204
|
+
produces?: ResourceType[]
|
|
205
|
+
/**
|
|
206
|
+
* Resource types this tool consumes (requires as input).
|
|
207
|
+
* Used for capability graph derivation — a tool that consumes
|
|
208
|
+
* 'integration.connection' can only run after a tool that produces it.
|
|
209
|
+
*/
|
|
210
|
+
consumes?: ResourceType[]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Edge in the capability graph connecting two tools via a resource.
|
|
215
|
+
* A tool that produces a resource connects to tools that consume it.
|
|
216
|
+
*
|
|
217
|
+
* @stub Implementation in PRD-00268 (buildCapabilityGraph)
|
|
218
|
+
*/
|
|
219
|
+
export interface CapabilityGraphEdge {
|
|
220
|
+
/** Tool ID that produces the resource */
|
|
221
|
+
from: string
|
|
222
|
+
/** Tool ID that consumes the resource */
|
|
223
|
+
to: string
|
|
224
|
+
/** Resource type flowing between tools */
|
|
225
|
+
resource: ResourceType
|
|
226
|
+
/** Resource domain (extracted from resource namespace prefix, e.g., 'slack' from 'slack.channel') */
|
|
227
|
+
domain?: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Named workflow derived from capability graph paths.
|
|
232
|
+
* Represents a common multi-tool sequence (e.g., "Connect Slack → Ingest → Query").
|
|
233
|
+
*
|
|
234
|
+
* @stub Implementation in PRD-00268 (buildCapabilityGraph)
|
|
235
|
+
*/
|
|
236
|
+
export interface ToolWorkflow {
|
|
237
|
+
/** Workflow name (e.g., "Slack Onboarding") */
|
|
238
|
+
name: string
|
|
239
|
+
/** Human-readable description of the workflow */
|
|
240
|
+
description: string
|
|
241
|
+
/** Ordered list of tool IDs in this workflow */
|
|
242
|
+
steps: string[]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Capability graph derived from tool resource flow metadata.
|
|
247
|
+
* Built from produces/consumes fields on MCPToolDescriptor.
|
|
248
|
+
*
|
|
249
|
+
* @stub Type definition only. buildCapabilityGraph() ships in PRD-00268.
|
|
250
|
+
*/
|
|
251
|
+
export interface CapabilityGraph {
|
|
252
|
+
/** Resource flow edges between tools */
|
|
253
|
+
edges: CapabilityGraphEdge[]
|
|
254
|
+
/** Named workflows derived from graph paths */
|
|
255
|
+
workflows: ToolWorkflow[]
|
|
88
256
|
}
|
|
89
257
|
|
|
90
258
|
/**
|
|
@@ -97,6 +265,13 @@ export interface MCPToolDescriptor {
|
|
|
97
265
|
export interface ToolDiscoveryResponse {
|
|
98
266
|
/** Tools available to the current user */
|
|
99
267
|
tools: MCPToolDescriptor[]
|
|
268
|
+
/**
|
|
269
|
+
* Capability graph derived from tool resource flow metadata.
|
|
270
|
+
* Optional — discovery responses may or may not include the computed graph.
|
|
271
|
+
*
|
|
272
|
+
* @stub Graph computation ships in PRD-00268 (buildCapabilityGraph).
|
|
273
|
+
*/
|
|
274
|
+
graph?: CapabilityGraph
|
|
100
275
|
}
|
|
101
276
|
|
|
102
277
|
/**
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource type vocabulary for MCP tool resource flow.
|
|
3
|
+
*
|
|
4
|
+
* Uses dot-separated namespacing consistent with trust/scopes.ts conventions.
|
|
5
|
+
* Each value represents a typed resource that tools produce or consume,
|
|
6
|
+
* enabling capability graph derivation from resource flow metadata.
|
|
7
|
+
*
|
|
8
|
+
* This is a CLOSED union — new resource types represent architectural
|
|
9
|
+
* additions that require a contracts release and downstream consumer updates.
|
|
10
|
+
* Unlike ToolIntegration, resource types are not extensible via (string & {}).
|
|
11
|
+
*/
|
|
12
|
+
export type ResourceType =
|
|
13
|
+
| 'integration.connection'
|
|
14
|
+
| 'slack.channel'
|
|
15
|
+
| 'slack.channel_scope'
|
|
16
|
+
| 'slack.coverage'
|
|
17
|
+
| 'ingestion.job'
|
|
18
|
+
| 'knowledge.fingerprint'
|
|
19
|
+
| 'org.status'
|
|
20
|
+
| 'system.status'
|
|
21
|
+
| 'system.runtime'
|
package/src/org/index.ts
CHANGED
|
@@ -68,10 +68,6 @@ export type {
|
|
|
68
68
|
|
|
69
69
|
export { ROLE_DISPLAY_MAP, TRANSFER_RESPONSIBILITIES, IDENTITY_TRUST_LEVEL_LABELS } from './types';
|
|
70
70
|
|
|
71
|
-
// Workspace capability types (Phase 3)
|
|
72
|
-
export type { WorkspaceCapability } from './capabilities';
|
|
73
|
-
export { WORKSPACE_CAPABILITIES, ROLE_CAPABILITY_MAP } from './capabilities';
|
|
74
|
-
|
|
75
71
|
// Domain types (Phase 4)
|
|
76
72
|
export type {
|
|
77
73
|
DomainStatus,
|
package/src/org/capabilities.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace Capability Types
|
|
3
|
-
*
|
|
4
|
-
* Capability constants for Phase 3 workspace expansion features.
|
|
5
|
-
* These define the permission boundaries for workspace actions.
|
|
6
|
-
*
|
|
7
|
-
* INVARIANTS:
|
|
8
|
-
* - Capabilities are checked server-side before any mutation
|
|
9
|
-
* - UI uses capabilities to gate action visibility
|
|
10
|
-
* - Capabilities map to RBAC roles (see RoleCapabilityMap)
|
|
11
|
-
*
|
|
12
|
-
* @see ADR-CONT-031 for design rationale
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// =============================================================================
|
|
16
|
-
// Workspace Capability Type
|
|
17
|
-
// =============================================================================
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Capabilities for workspace actions.
|
|
21
|
-
* Used for capability-based access control in Phase 3 features.
|
|
22
|
-
*
|
|
23
|
-
* Capability hierarchy (implicit):
|
|
24
|
-
* - owner: all capabilities
|
|
25
|
-
* - admin: invite_member, manage_members (limited), manage_auth, demote_integration (own only)
|
|
26
|
-
* - member: none (read-only)
|
|
27
|
-
*/
|
|
28
|
-
export type WorkspaceCapability =
|
|
29
|
-
// Member management
|
|
30
|
-
| 'org.invite_member'
|
|
31
|
-
| 'org.manage_members'
|
|
32
|
-
// Integration management
|
|
33
|
-
| 'org.promote_integration'
|
|
34
|
-
| 'org.demote_integration'
|
|
35
|
-
// Auth policy
|
|
36
|
-
| 'org.manage_auth'
|
|
37
|
-
// Domain claiming (future)
|
|
38
|
-
| 'org.claim_domain';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* All workspace capabilities.
|
|
42
|
-
* Use for iteration and validation.
|
|
43
|
-
*/
|
|
44
|
-
export const WORKSPACE_CAPABILITIES: readonly WorkspaceCapability[] = [
|
|
45
|
-
'org.invite_member',
|
|
46
|
-
'org.manage_members',
|
|
47
|
-
'org.promote_integration',
|
|
48
|
-
'org.demote_integration',
|
|
49
|
-
'org.manage_auth',
|
|
50
|
-
'org.claim_domain',
|
|
51
|
-
] as const;
|
|
52
|
-
|
|
53
|
-
// =============================================================================
|
|
54
|
-
// Role → Capability Mapping
|
|
55
|
-
// =============================================================================
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Capabilities granted to each workspace role.
|
|
59
|
-
*
|
|
60
|
-
* INVARIANTS:
|
|
61
|
-
* - Owner has all capabilities (cannot be restricted)
|
|
62
|
-
* - Admin cannot demote other admins (enforce in service layer)
|
|
63
|
-
* - Member has no mutation capabilities
|
|
64
|
-
*
|
|
65
|
-
* @see Phase 3 Invariant #4: Admin floor
|
|
66
|
-
* @see Phase 3 Invariant #5: Admin ≠ owner
|
|
67
|
-
*/
|
|
68
|
-
export const ROLE_CAPABILITY_MAP = {
|
|
69
|
-
owner: [
|
|
70
|
-
'org.invite_member',
|
|
71
|
-
'org.manage_members',
|
|
72
|
-
'org.promote_integration',
|
|
73
|
-
'org.demote_integration',
|
|
74
|
-
'org.manage_auth',
|
|
75
|
-
'org.claim_domain',
|
|
76
|
-
],
|
|
77
|
-
admin: [
|
|
78
|
-
'org.invite_member',
|
|
79
|
-
'org.manage_members', // Note: cannot remove/demote other admins
|
|
80
|
-
'org.manage_auth',
|
|
81
|
-
'org.demote_integration', // Can demote own integrations only
|
|
82
|
-
],
|
|
83
|
-
member: [],
|
|
84
|
-
} as const satisfies Record<string, readonly WorkspaceCapability[]>;
|