@contractspec/bundle.library 3.8.4 → 3.8.7

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.
Files changed (90) hide show
  1. package/.turbo/turbo-build.log +126 -112
  2. package/CHANGELOG.md +56 -0
  3. package/dist/application/index.js +806 -131
  4. package/dist/application/mcp/cliMcp.js +21 -2
  5. package/dist/application/mcp/common.js +21 -2
  6. package/dist/application/mcp/common.test.d.ts +1 -0
  7. package/dist/application/mcp/contractsMcp.js +21 -2
  8. package/dist/application/mcp/docsMcp.catalog.d.ts +2 -0
  9. package/dist/application/mcp/docsMcp.catalog.js +382 -0
  10. package/dist/application/mcp/docsMcp.d.ts +5 -1
  11. package/dist/application/mcp/docsMcp.data.d.ts +85 -0
  12. package/dist/application/mcp/docsMcp.data.js +148 -0
  13. package/dist/application/mcp/docsMcp.js +776 -101
  14. package/dist/application/mcp/docsMcp.prompts.d.ts +3 -0
  15. package/dist/application/mcp/docsMcp.prompts.js +522 -0
  16. package/dist/application/mcp/docsMcp.reference.d.ts +24 -0
  17. package/dist/application/mcp/docsMcp.reference.js +236 -0
  18. package/dist/application/mcp/docsMcp.resources.d.ts +3 -0
  19. package/dist/application/mcp/docsMcp.resources.js +520 -0
  20. package/dist/application/mcp/docsMcp.test.d.ts +1 -0
  21. package/dist/application/mcp/docsMcp.tools.d.ts +3 -0
  22. package/dist/application/mcp/docsMcp.tools.js +519 -0
  23. package/dist/application/mcp/index.js +806 -131
  24. package/dist/application/mcp/internalMcp.js +21 -2
  25. package/dist/application/mcp/normalizeMcpRequest.d.ts +1 -0
  26. package/dist/application/mcp/normalizeMcpRequest.js +22 -0
  27. package/dist/application/mcp/providerRankingMcp.js +21 -2
  28. package/dist/features/index.js +15 -15
  29. package/dist/index.js +171 -171
  30. package/dist/node/application/index.js +806 -131
  31. package/dist/node/application/mcp/cliMcp.js +21 -2
  32. package/dist/node/application/mcp/common.js +21 -2
  33. package/dist/node/application/mcp/contractsMcp.js +21 -2
  34. package/dist/node/application/mcp/docsMcp.catalog.js +381 -0
  35. package/dist/node/application/mcp/docsMcp.data.js +147 -0
  36. package/dist/node/application/mcp/docsMcp.js +776 -101
  37. package/dist/node/application/mcp/docsMcp.prompts.js +521 -0
  38. package/dist/node/application/mcp/docsMcp.reference.js +235 -0
  39. package/dist/node/application/mcp/docsMcp.resources.js +519 -0
  40. package/dist/node/application/mcp/docsMcp.tools.js +518 -0
  41. package/dist/node/application/mcp/index.js +806 -131
  42. package/dist/node/application/mcp/internalMcp.js +21 -2
  43. package/dist/node/application/mcp/normalizeMcpRequest.js +21 -0
  44. package/dist/node/application/mcp/providerRankingMcp.js +21 -2
  45. package/dist/node/features/index.js +15 -15
  46. package/dist/node/index.js +171 -171
  47. package/dist/node/presentation/features/hooks/index.js +12 -12
  48. package/dist/node/presentation/features/hooks/useContractsRegistry.js +12 -12
  49. package/dist/node/presentation/features/index.js +12 -12
  50. package/dist/node/presentation/features/organisms/FeatureDataViewsList.js +12 -12
  51. package/dist/node/presentation/features/organisms/FeatureEventsList.js +12 -12
  52. package/dist/node/presentation/features/organisms/FeatureFormsList.js +12 -12
  53. package/dist/node/presentation/features/organisms/FeaturePresentationsList.js +12 -12
  54. package/dist/node/presentation/features/organisms/index.js +12 -12
  55. package/dist/node/presentation/features/templates/FeatureDataViewsTemplate/FeatureDataViewsTemplate.js +12 -12
  56. package/dist/node/presentation/features/templates/FeatureDataViewsTemplate/index.js +12 -12
  57. package/dist/node/presentation/features/templates/FeatureEventsTemplate/FeatureEventsTemplate.js +12 -12
  58. package/dist/node/presentation/features/templates/FeatureEventsTemplate/index.js +12 -12
  59. package/dist/node/presentation/features/templates/FeatureFormsTemplate/FeatureFormsTemplate.js +12 -12
  60. package/dist/node/presentation/features/templates/FeatureFormsTemplate/index.js +12 -12
  61. package/dist/node/presentation/features/templates/FeaturePresentationsTemplate/FeaturePresentationsTemplate.js +12 -12
  62. package/dist/node/presentation/features/templates/FeaturePresentationsTemplate/index.js +12 -12
  63. package/dist/presentation/features/hooks/index.js +12 -12
  64. package/dist/presentation/features/hooks/useContractsRegistry.js +12 -12
  65. package/dist/presentation/features/index.js +12 -12
  66. package/dist/presentation/features/organisms/FeatureDataViewsList.js +12 -12
  67. package/dist/presentation/features/organisms/FeatureEventsList.js +12 -12
  68. package/dist/presentation/features/organisms/FeatureFormsList.js +12 -12
  69. package/dist/presentation/features/organisms/FeaturePresentationsList.js +12 -12
  70. package/dist/presentation/features/organisms/index.js +12 -12
  71. package/dist/presentation/features/templates/FeatureDataViewsTemplate/FeatureDataViewsTemplate.js +12 -12
  72. package/dist/presentation/features/templates/FeatureDataViewsTemplate/index.js +12 -12
  73. package/dist/presentation/features/templates/FeatureEventsTemplate/FeatureEventsTemplate.js +12 -12
  74. package/dist/presentation/features/templates/FeatureEventsTemplate/index.js +12 -12
  75. package/dist/presentation/features/templates/FeatureFormsTemplate/FeatureFormsTemplate.js +12 -12
  76. package/dist/presentation/features/templates/FeatureFormsTemplate/index.js +12 -12
  77. package/dist/presentation/features/templates/FeaturePresentationsTemplate/FeaturePresentationsTemplate.js +12 -12
  78. package/dist/presentation/features/templates/FeaturePresentationsTemplate/index.js +12 -12
  79. package/package.json +108 -24
  80. package/src/application/mcp/common.test.ts +64 -0
  81. package/src/application/mcp/common.ts +5 -2
  82. package/src/application/mcp/docsMcp.catalog.ts +2 -0
  83. package/src/application/mcp/docsMcp.data.ts +196 -0
  84. package/src/application/mcp/docsMcp.prompts.ts +165 -0
  85. package/src/application/mcp/docsMcp.reference.ts +152 -0
  86. package/src/application/mcp/docsMcp.resources.ts +194 -0
  87. package/src/application/mcp/docsMcp.test.ts +148 -0
  88. package/src/application/mcp/docsMcp.tools.ts +183 -0
  89. package/src/application/mcp/docsMcp.ts +13 -177
  90. package/src/application/mcp/normalizeMcpRequest.ts +30 -0
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { Elysia } from 'elysia';
3
+ import { createDocsMcpHandler } from './docsMcp';
4
+
5
+ function createTestApp() {
6
+ return new Elysia().use(createDocsMcpHandler('/mcp/docs'));
7
+ }
8
+
9
+ async function mcpRequest(
10
+ method: string,
11
+ params: Record<string, unknown> = {}
12
+ ) {
13
+ return createTestApp().handle(
14
+ new Request('http://localhost/mcp/docs', {
15
+ method: 'POST',
16
+ headers: {
17
+ accept: 'application/json',
18
+ 'content-type': 'application/json',
19
+ },
20
+ body: JSON.stringify({
21
+ jsonrpc: '2.0',
22
+ id: 1,
23
+ method,
24
+ params,
25
+ }),
26
+ })
27
+ );
28
+ }
29
+
30
+ async function initialize() {
31
+ const response = await mcpRequest('initialize', {
32
+ protocolVersion: '2024-11-05',
33
+ capabilities: {},
34
+ clientInfo: { name: 'docs-mcp-test', version: '1.0.0' },
35
+ });
36
+ expect(response.status).toBe(200);
37
+ }
38
+
39
+ describe('docs MCP handler', () => {
40
+ it('exposes curated tools, prompts, and resources', async () => {
41
+ await initialize();
42
+
43
+ const toolsResponse = await mcpRequest('tools/list');
44
+ const toolsBody = await toolsResponse.json();
45
+ const toolNames = toolsBody.result.tools.map(
46
+ (tool: { name: string }) => tool.name
47
+ );
48
+ expect(toolNames).toContain('docs_search-v1_0_0');
49
+ expect(toolNames).toContain('docs_get-v1_0_0');
50
+ expect(toolNames).toContain('docs_resolve_route-v1_0_0');
51
+ expect(toolNames).toContain('docs_contract_reference-v1_0_0');
52
+ expect(toolNames).toContain('docs_list_facets-v1_0_0');
53
+
54
+ const promptsResponse = await mcpRequest('prompts/list');
55
+ const promptsBody = await promptsResponse.json();
56
+ const promptNames = promptsBody.result.prompts.map(
57
+ (prompt: { name: string }) => prompt.name
58
+ );
59
+ expect(promptNames).toContain('docs_navigator');
60
+ expect(promptNames).toContain('docs_reference_guide');
61
+
62
+ const listedResourcesResponse = await mcpRequest('resources/list');
63
+ const listedResourcesBody = await listedResourcesResponse.json();
64
+ const listedResources = listedResourcesBody.result.resources ?? [];
65
+ const listedResourceUris = listedResources.map(
66
+ (resource: { uri?: string; uriTemplate?: string }) =>
67
+ resource.uriTemplate ?? resource.uri
68
+ );
69
+ expect(listedResourceUris).toContain('docs://index');
70
+ expect(listedResourceUris).toContain('docs://list');
71
+ expect(listedResourceUris).toContain('docs://facets');
72
+
73
+ const templatesResponse = await mcpRequest('resources/templates/list');
74
+ const templatesBody = await templatesResponse.json();
75
+ const templates = templatesBody.result.resourceTemplates ?? [];
76
+ const templateUris = templates.map(
77
+ (resource: { uri?: string; uriTemplate?: string }) =>
78
+ resource.uriTemplate ?? resource.uri
79
+ );
80
+ expect(templateUris).toContain(
81
+ 'docs://index{?query,tag,kind,visibility,limit,offset}'
82
+ );
83
+ expect(templateUris).toContain(
84
+ 'docs://contract-reference/{key}{?version,type,includeSchema}'
85
+ );
86
+ expect(
87
+ listedResourceUris.some((uri: string) =>
88
+ uri.startsWith('presentation://')
89
+ )
90
+ ).toBeFalse();
91
+ });
92
+
93
+ it('supports paginated docs search and contract reference lookup', async () => {
94
+ await initialize();
95
+
96
+ const searchResponse = await mcpRequest('tools/call', {
97
+ name: 'docs_search-v1_0_0',
98
+ arguments: {
99
+ query: 'docs',
100
+ kind: 'how',
101
+ limit: 1,
102
+ },
103
+ });
104
+ const searchBody = await searchResponse.json();
105
+ const searchResult = JSON.parse(searchBody.result.content[0].text);
106
+ expect(searchResult.docs).toBeArray();
107
+ expect(searchResult.docs.length).toBe(1);
108
+ expect(searchResult.total).toBeGreaterThan(1);
109
+ expect(searchResult.nextOffset).toBe(1);
110
+
111
+ const referenceResponse = await mcpRequest('tools/call', {
112
+ name: 'docs_contract_reference-v1_0_0',
113
+ arguments: {
114
+ key: 'docs.generate',
115
+ type: 'command',
116
+ includeSchema: true,
117
+ },
118
+ });
119
+ const referenceBody = await referenceResponse.json();
120
+ const referenceResult = JSON.parse(referenceBody.result.content[0].text);
121
+ expect(referenceResult.reference.type).toBe('command');
122
+ expect(referenceResult.reference.route).toBe('/docs/tech/docs/generator');
123
+ expect(referenceResult.reference.schema.meta.key).toBe('docs.generate');
124
+ });
125
+
126
+ it('keeps markdown doc resources and richer prompts usable', async () => {
127
+ await initialize();
128
+
129
+ const docResponse = await mcpRequest('resources/read', {
130
+ uri: 'docs://doc/docs.tech.docs-generator',
131
+ });
132
+ const docBody = await docResponse.json();
133
+ expect(docBody.result.contents[0].text).toContain('# Docs generator');
134
+
135
+ const promptResponse = await mcpRequest('prompts/get', {
136
+ name: 'docs_reference_guide',
137
+ arguments: {
138
+ key: 'docs.generate',
139
+ type: 'command',
140
+ },
141
+ });
142
+ const promptBody = await promptResponse.json();
143
+ const text = promptBody.result.messages[0].content.text;
144
+ expect(text).toContain('docs_contract_reference-v1_0_0');
145
+ expect(text).toContain('docs.generate');
146
+ expect(text).toContain('docs://contract-reference/docs.generate');
147
+ });
148
+ });
@@ -0,0 +1,183 @@
1
+ import {
2
+ defineCommand,
3
+ defineSchemaModel,
4
+ installOp,
5
+ OperationSpecRegistry,
6
+ } from '@contractspec/lib.contracts-spec';
7
+ import type { DocPresentationRoute } from '@contractspec/lib.contracts-spec/docs';
8
+ import {
9
+ ContractReferenceInput,
10
+ ContractReferenceOutput,
11
+ DocsIndexInput,
12
+ DocsIndexOutput,
13
+ } from '@contractspec/lib.contracts-spec/docs';
14
+ import { ScalarTypeEnum } from '@contractspec/lib.schema';
15
+ import {
16
+ getDocById,
17
+ getDocByRoute,
18
+ listDocFacets,
19
+ searchDocs,
20
+ } from './docsMcp.data';
21
+ import { resolveContractReference } from './docsMcp.reference';
22
+
23
+ const DOC_OWNERS = ['@contractspec'];
24
+ const DOC_TAGS = ['docs', 'mcp'];
25
+
26
+ const DocsGetInput = defineSchemaModel({
27
+ name: 'DocsGetInput',
28
+ fields: {
29
+ id: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
30
+ },
31
+ });
32
+
33
+ const DocsGetOutput = defineSchemaModel({
34
+ name: 'DocsGetOutput',
35
+ fields: {
36
+ doc: { type: ScalarTypeEnum.JSON(), isOptional: false },
37
+ content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
38
+ },
39
+ });
40
+
41
+ const DocsResolveRouteInput = defineSchemaModel({
42
+ name: 'DocsResolveRouteInput',
43
+ fields: {
44
+ route: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
45
+ },
46
+ });
47
+
48
+ const DocsResolveRouteOutput = defineSchemaModel({
49
+ name: 'DocsResolveRouteOutput',
50
+ fields: {
51
+ doc: { type: ScalarTypeEnum.JSON(), isOptional: false },
52
+ content: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
53
+ },
54
+ });
55
+
56
+ const DocsFacetsInput = defineSchemaModel({
57
+ name: 'DocsFacetsInput',
58
+ fields: {},
59
+ });
60
+
61
+ const DocsFacetsOutput = defineSchemaModel({
62
+ name: 'DocsFacetsOutput',
63
+ fields: {
64
+ facets: { type: ScalarTypeEnum.JSON(), isOptional: false },
65
+ },
66
+ });
67
+
68
+ export function buildDocOps(routes: DocPresentationRoute[]) {
69
+ const registry = new OperationSpecRegistry();
70
+
71
+ installOp(
72
+ registry,
73
+ defineCommand({
74
+ meta: {
75
+ key: 'docs.search',
76
+ version: '1.0.0',
77
+ stability: 'beta',
78
+ owners: DOC_OWNERS,
79
+ tags: DOC_TAGS,
80
+ description:
81
+ 'Search ContractSpec docs by query, tag, kind, or visibility.',
82
+ goal: 'Find the most relevant DocBlocks without browsing the full corpus.',
83
+ context: 'Read-only docs MCP search surface.',
84
+ },
85
+ io: { input: DocsIndexInput, output: DocsIndexOutput },
86
+ policy: { auth: 'anonymous' },
87
+ transport: { mcp: { toolName: 'docs_search-v1_0_0' } },
88
+ }),
89
+ async (args) => searchDocs(routes, args)
90
+ );
91
+
92
+ installOp(
93
+ registry,
94
+ defineCommand({
95
+ meta: {
96
+ key: 'docs.get',
97
+ version: '1.0.0',
98
+ stability: 'beta',
99
+ owners: DOC_OWNERS,
100
+ tags: DOC_TAGS,
101
+ description: 'Read a single DocBlock by id.',
102
+ goal: 'Fetch the exact markdown content and metadata for a known doc id.',
103
+ context: 'Read-only docs MCP surface.',
104
+ },
105
+ io: { input: DocsGetInput, output: DocsGetOutput },
106
+ policy: { auth: 'anonymous' },
107
+ transport: { mcp: { toolName: 'docs_get-v1_0_0' } },
108
+ }),
109
+ async ({ id }) => {
110
+ const found = getDocById(id);
111
+ if (!found) throw new Error(`DocBlock not found: ${id}`);
112
+ return found;
113
+ }
114
+ );
115
+
116
+ installOp(
117
+ registry,
118
+ defineCommand({
119
+ meta: {
120
+ key: 'docs.resolveRoute',
121
+ version: '1.0.0',
122
+ stability: 'beta',
123
+ owners: DOC_OWNERS,
124
+ tags: DOC_TAGS,
125
+ description: 'Resolve a docs route to the matching DocBlock.',
126
+ goal: 'Turn a route or URL path into a canonical doc id and markdown body.',
127
+ context: 'Read-only docs MCP surface.',
128
+ },
129
+ io: { input: DocsResolveRouteInput, output: DocsResolveRouteOutput },
130
+ policy: { auth: 'anonymous' },
131
+ transport: { mcp: { toolName: 'docs_resolve_route-v1_0_0' } },
132
+ }),
133
+ async ({ route }) => {
134
+ const found = getDocByRoute(routes, route);
135
+ if (!found) throw new Error(`Doc route not found: ${route}`);
136
+ return found;
137
+ }
138
+ );
139
+
140
+ installOp(
141
+ registry,
142
+ defineCommand({
143
+ meta: {
144
+ key: 'docs.contract.lookup',
145
+ version: '1.0.0',
146
+ stability: 'beta',
147
+ owners: DOC_OWNERS,
148
+ tags: DOC_TAGS,
149
+ description:
150
+ 'Resolve a ContractSpec surface into a docs-ready reference payload.',
151
+ goal: 'Get canonical docs metadata, route, and optional schema for a spec key.',
152
+ context: 'Read-only docs MCP surface.',
153
+ },
154
+ io: { input: ContractReferenceInput, output: ContractReferenceOutput },
155
+ policy: { auth: 'anonymous' },
156
+ transport: { mcp: { toolName: 'docs_contract_reference-v1_0_0' } },
157
+ }),
158
+ async (args) => resolveContractReference(args)
159
+ );
160
+
161
+ installOp(
162
+ registry,
163
+ defineCommand({
164
+ meta: {
165
+ key: 'docs.list.facets',
166
+ version: '1.0.0',
167
+ stability: 'beta',
168
+ owners: DOC_OWNERS,
169
+ tags: DOC_TAGS,
170
+ description:
171
+ 'List docs taxonomy facets such as tags, kinds, and visibilities.',
172
+ goal: 'Help agents browse the docs corpus before making targeted reads.',
173
+ context: 'Read-only docs MCP surface.',
174
+ },
175
+ io: { input: DocsFacetsInput, output: DocsFacetsOutput },
176
+ policy: { auth: 'anonymous' },
177
+ transport: { mcp: { toolName: 'docs_list_facets-v1_0_0' } },
178
+ }),
179
+ async () => ({ facets: listDocFacets(routes) })
180
+ );
181
+
182
+ return registry;
183
+ }
@@ -1,184 +1,18 @@
1
- import {
2
- definePrompt,
3
- defineResourceTemplate,
4
- installOp,
5
- OperationSpecRegistry,
6
- PromptRegistry,
7
- ResourceRegistry,
8
- } from '@contractspec/lib.contracts-spec';
9
- import type { DocPresentationRoute } from '@contractspec/lib.contracts-spec/docs';
10
1
  import { defaultDocRegistry } from '@contractspec/lib.contracts-spec/docs';
11
- import z from 'zod';
12
- import { docsSearchSpec } from '../../features/docs';
13
2
  import { appLogger } from '../../infrastructure/elysia/logger';
14
3
  import { createMcpElysiaHandler } from './common';
4
+ import { buildDocPrompts } from './docsMcp.prompts';
5
+ import { buildDocResources } from './docsMcp.resources';
6
+ import { buildDocOps } from './docsMcp.tools';
15
7
 
16
- const DOC_OWNERS = ['@contractspec'];
17
- const DOC_TAGS = ['docs', 'mcp'];
18
-
19
- function buildDocResources(routes: DocPresentationRoute[]) {
20
- const resources = new ResourceRegistry();
21
-
22
- resources.register(
23
- defineResourceTemplate({
24
- meta: {
25
- uriTemplate: 'docs://list',
26
- title: 'DocBlocks index',
27
- description:
28
- 'All registered DocBlocks with route, visibility, tags, and summary.',
29
- mimeType: 'application/json',
30
- tags: DOC_TAGS,
31
- },
32
- input: z.object({}),
33
- resolve: async () => {
34
- const docs = routes.map(({ block, route }) => ({
35
- id: block.id,
36
- title: block.title,
37
- summary: block.summary ?? '',
38
- tags: block.tags ?? [],
39
- visibility: block.visibility ?? 'public',
40
- route,
41
- }));
42
-
43
- return {
44
- uri: 'docs://list',
45
- mimeType: 'application/json',
46
- data: JSON.stringify(docs, null, 2),
47
- };
48
- },
49
- })
50
- );
51
-
52
- resources.register(
53
- defineResourceTemplate({
54
- meta: {
55
- uriTemplate: 'docs://doc/{id}',
56
- title: 'DocBlock markdown',
57
- description: 'Fetch DocBlock body by id as markdown.',
58
- mimeType: 'text/markdown',
59
- tags: DOC_TAGS,
60
- },
61
- input: z.object({ id: z.string() }),
62
- resolve: async ({ id }) => {
63
- const found = defaultDocRegistry.get(id);
64
- if (!found) {
65
- return {
66
- uri: `docs://doc/${encodeURIComponent(id)}`,
67
- mimeType: 'text/plain',
68
- data: `DocBlock not found: ${id}`,
69
- };
70
- }
71
-
72
- return {
73
- uri: `docs://doc/${encodeURIComponent(id)}`,
74
- mimeType: 'text/markdown',
75
- data: String(found.block.body ?? ''),
76
- };
77
- },
78
- })
79
- );
80
-
81
- return resources;
82
- }
83
-
84
- function buildDocPrompts() {
85
- const prompts = new PromptRegistry();
86
-
87
- prompts.register(
88
- definePrompt({
89
- meta: {
90
- key: 'docs.navigator',
91
- version: '1.0.0',
92
- title: 'Find relevant ContractSpec docs',
93
- description: 'Guide agents to pick the right DocBlock by topic or tag.',
94
- tags: DOC_TAGS,
95
- stability: 'beta',
96
- owners: DOC_OWNERS,
97
- },
98
- args: [
99
- {
100
- name: 'topic',
101
- description: 'Goal or subject to search for.',
102
- required: false,
103
- schema: z.string().optional(),
104
- },
105
- {
106
- name: 'tag',
107
- description: 'Optional tag filter.',
108
- required: false,
109
- schema: z.string().optional(),
110
- },
111
- ],
112
- input: z.object({
113
- topic: z.string().optional(),
114
- tag: z.string().optional(),
115
- }),
116
- render: async ({ topic, tag }) => {
117
- const parts = [
118
- {
119
- type: 'text' as const,
120
- text: `Use the docs index to choose DocBlocks. If a specific topic is provided, prefer docs whose id/title/summary match it.${topic ? ` Topic: ${topic}.` : ''}${tag ? ` Tag: ${tag}.` : ''}`,
121
- },
122
- {
123
- type: 'resource' as const,
124
- uri: 'docs://list',
125
- title: 'DocBlocks index',
126
- },
127
- ];
128
- return parts;
129
- },
130
- })
131
- );
132
-
133
- return prompts;
134
- }
135
-
136
- function buildDocOps(routes: DocPresentationRoute[]) {
137
- const registry = new OperationSpecRegistry();
138
-
139
- // Use the module-level spec from docs.contracts.ts
140
- installOp(registry, docsSearchSpec, async (args) => {
141
- const query = args.query?.toLowerCase().trim();
142
- const tagsFilter = args.tag?.map((t) => t.toLowerCase().trim()) ?? [];
143
- const visibility = args.visibility?.toLowerCase().trim();
144
-
145
- const docs = routes
146
- .map(({ block, route }) => ({
147
- id: block.id,
148
- title: block.title,
149
- summary: block.summary ?? '',
150
- tags: block.tags ?? [],
151
- visibility: (block.visibility ?? 'public').toLowerCase(),
152
- route,
153
- }))
154
- .filter((doc) => {
155
- const matchesQuery = query
156
- ? doc.id.toLowerCase().includes(query) ||
157
- doc.title.toLowerCase().includes(query) ||
158
- doc.summary.toLowerCase().includes(query)
159
- : true;
160
- const matchesTags = tagsFilter.length
161
- ? tagsFilter.every((t) =>
162
- doc.tags.some((tag) => tag.toLowerCase().includes(t))
163
- )
164
- : true;
165
- const matchesVisibility = visibility
166
- ? doc.visibility === visibility
167
- : true;
168
- return matchesQuery && matchesTags && matchesVisibility;
169
- });
170
-
171
- return {
172
- docs,
173
- items: docs,
174
- total: docs.length,
175
- };
176
- });
177
-
178
- return registry;
8
+ interface DocsMcpHandlerOptions {
9
+ includePresentations?: boolean;
179
10
  }
180
11
 
181
- export function createDocsMcpHandler(path = '/api/mcp/docs') {
12
+ export function createDocsMcpHandler(
13
+ path = '/api/mcp/docs',
14
+ options: DocsMcpHandlerOptions = {}
15
+ ) {
182
16
  const routes = defaultDocRegistry.list();
183
17
 
184
18
  return createMcpElysiaHandler({
@@ -187,7 +21,9 @@ export function createDocsMcpHandler(path = '/api/mcp/docs') {
187
21
  serverName: 'contractspec-docs-mcp',
188
22
  ops: buildDocOps(routes),
189
23
  resources: buildDocResources(routes),
190
- prompts: buildDocPrompts(),
191
- presentations: routes.map(({ descriptor }) => descriptor),
24
+ prompts: buildDocPrompts(routes),
25
+ presentations: options.includePresentations
26
+ ? routes.map(({ descriptor }) => descriptor)
27
+ : undefined,
192
28
  });
193
29
  }
@@ -0,0 +1,30 @@
1
+ const REQUIRED_ACCEPT_TYPES = ['application/json', 'text/event-stream'];
2
+
3
+ function canNormalizeAcceptHeader(acceptHeader: string | null) {
4
+ return (
5
+ !acceptHeader ||
6
+ acceptHeader.includes('*/*') ||
7
+ acceptHeader.includes('application/*') ||
8
+ REQUIRED_ACCEPT_TYPES.some((value) => acceptHeader.includes(value))
9
+ );
10
+ }
11
+
12
+ export function normalizeMcpRequest(request: Request) {
13
+ if (request.method !== 'POST') return request;
14
+
15
+ const acceptHeader = request.headers.get('accept');
16
+ if (!canNormalizeAcceptHeader(acceptHeader)) return request;
17
+
18
+ const missingTypes = REQUIRED_ACCEPT_TYPES.filter(
19
+ (value) => !acceptHeader?.includes(value)
20
+ );
21
+ if (missingTypes.length === 0) return request;
22
+
23
+ const headers = new Headers(request.headers);
24
+ headers.set(
25
+ 'accept',
26
+ [acceptHeader, ...missingTypes].filter(Boolean).join(', ')
27
+ );
28
+
29
+ return new Request(request, { headers });
30
+ }