strapi-mcp-server 0.1.1
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/LICENSE +21 -0
- package/README.md +415 -0
- package/admin/src/components/PageHeader.tsx +33 -0
- package/admin/src/components/Sidebar.tsx +138 -0
- package/admin/src/index.tsx +54 -0
- package/admin/src/lib/api.ts +27 -0
- package/admin/src/lib/applyQuery.ts +152 -0
- package/admin/src/pages/App.tsx +126 -0
- package/admin/src/pages/AuditLog.tsx +386 -0
- package/admin/src/pages/Clients.tsx +465 -0
- package/admin/src/pages/EditClient.tsx +248 -0
- package/admin/src/pages/HomePage.tsx +378 -0
- package/admin/src/pages/NewClient.tsx +244 -0
- package/admin/src/pages/Settings.tsx +514 -0
- package/admin/src/pages/SsoBridge.tsx +96 -0
- package/admin/src/pages/Tools.tsx +68 -0
- package/admin/src/pluginId.ts +1 -0
- package/admin/src/translations/en.json +8 -0
- package/package.json +105 -0
- package/server/src/bootstrap.ts +118 -0
- package/server/src/config/index.ts +290 -0
- package/server/src/content-types/audit-log/index.ts +3 -0
- package/server/src/content-types/audit-log/schema.json +32 -0
- package/server/src/content-types/index.ts +19 -0
- package/server/src/content-types/oauth-auth-code/index.ts +3 -0
- package/server/src/content-types/oauth-auth-code/schema.json +31 -0
- package/server/src/content-types/oauth-client/index.ts +3 -0
- package/server/src/content-types/oauth-client/schema.json +33 -0
- package/server/src/content-types/oauth-consent/index.ts +3 -0
- package/server/src/content-types/oauth-consent/schema.json +21 -0
- package/server/src/content-types/oauth-refresh-token/index.ts +3 -0
- package/server/src/content-types/oauth-refresh-token/schema.json +25 -0
- package/server/src/content-types/oauth-revocation/index.ts +3 -0
- package/server/src/content-types/oauth-revocation/schema.json +18 -0
- package/server/src/content-types/oauth-signing-key/index.ts +3 -0
- package/server/src/content-types/oauth-signing-key/schema.json +21 -0
- package/server/src/controllers/admin/audit.ts +30 -0
- package/server/src/controllers/admin/clients.ts +148 -0
- package/server/src/controllers/admin/dashboard.ts +28 -0
- package/server/src/controllers/admin/index.ts +15 -0
- package/server/src/controllers/admin/settings.ts +38 -0
- package/server/src/controllers/admin/tools.ts +23 -0
- package/server/src/controllers/index.ts +13 -0
- package/server/src/controllers/mcp.ts +168 -0
- package/server/src/controllers/oauth/authorize.ts +418 -0
- package/server/src/controllers/oauth/index.ts +15 -0
- package/server/src/controllers/oauth/introspect.ts +45 -0
- package/server/src/controllers/oauth/metadata.ts +86 -0
- package/server/src/controllers/oauth/mode-guard.ts +22 -0
- package/server/src/controllers/oauth/register.ts +109 -0
- package/server/src/controllers/oauth/token.ts +206 -0
- package/server/src/controllers/proxy.ts +81 -0
- package/server/src/destroy.ts +28 -0
- package/server/src/index.ts +23 -0
- package/server/src/policies/authenticate.ts +81 -0
- package/server/src/policies/index.ts +13 -0
- package/server/src/policies/origin.ts +50 -0
- package/server/src/policies/rateLimit.ts +27 -0
- package/server/src/policies/scope.ts +32 -0
- package/server/src/register.ts +48 -0
- package/server/src/routes/admin.ts +85 -0
- package/server/src/routes/index.ts +13 -0
- package/server/src/routes/mcp.ts +31 -0
- package/server/src/routes/oauth.ts +81 -0
- package/server/src/routes/proxy.ts +29 -0
- package/server/src/services/audit.ts +158 -0
- package/server/src/services/heartbeat.ts +76 -0
- package/server/src/services/index.ts +37 -0
- package/server/src/services/instance-id.ts +30 -0
- package/server/src/services/mcp-server.ts +100 -0
- package/server/src/services/oauth/audience.ts +26 -0
- package/server/src/services/oauth/auth-codes.ts +78 -0
- package/server/src/services/oauth/clients.ts +386 -0
- package/server/src/services/oauth/consent.ts +38 -0
- package/server/src/services/oauth/errors.ts +32 -0
- package/server/src/services/oauth/pkce.ts +34 -0
- package/server/src/services/oauth/scopes.ts +42 -0
- package/server/src/services/oauth/signing-keys.ts +166 -0
- package/server/src/services/oauth/tokens.ts +324 -0
- package/server/src/services/permissions.ts +87 -0
- package/server/src/services/proxy-client.ts +167 -0
- package/server/src/services/rate-limiter.ts +180 -0
- package/server/src/services/redis.ts +139 -0
- package/server/src/services/session-directory.ts +121 -0
- package/server/src/services/session-store.ts +216 -0
- package/server/src/services/sso-cookie.ts +146 -0
- package/server/src/services/tools/content.ts +284 -0
- package/server/src/services/tools/index.ts +23 -0
- package/server/src/services/tools/media.ts +170 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
5
|
+
import { writeFile, mkdtemp, rm } from 'fs/promises';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { request } from 'undici';
|
|
9
|
+
import type { ToolDef, ToolFactoryArgs } from './content';
|
|
10
|
+
import { getConfig } from '../../config';
|
|
11
|
+
import { hasScope, type Scope } from '../oauth/scopes';
|
|
12
|
+
|
|
13
|
+
const MIME_RE = /^[\w.-]+\/[\w.+-]+$/;
|
|
14
|
+
const FILENAME_RE = /^[A-Za-z0-9._\- ()]{1,255}$/;
|
|
15
|
+
const MAX_BASE64_LEN = 20_000_000; // ~15 MB decoded; matches default 10 MB upload cap with headroom
|
|
16
|
+
|
|
17
|
+
export function createMediaTools(args: ToolFactoryArgs): ToolDef[] {
|
|
18
|
+
const { strapi, scopes } = args;
|
|
19
|
+
const cfg = getConfig(strapi);
|
|
20
|
+
|
|
21
|
+
function requireScope(s: Scope): void {
|
|
22
|
+
if (!hasScope(scopes, s)) {
|
|
23
|
+
const err = new Error('You do not have permission to perform this action.');
|
|
24
|
+
(err as Error & { code?: string }).code = 'insufficient_scope';
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const json = (value: unknown) => ({
|
|
30
|
+
content: [{ type: 'text' as const, text: JSON.stringify(value) }],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
name: 'strapi.media.list',
|
|
36
|
+
description: 'Paginated list of uploaded files.',
|
|
37
|
+
scope: 'strapi:media:read',
|
|
38
|
+
inputSchema: z
|
|
39
|
+
.object({
|
|
40
|
+
page: z.number().int().min(1).max(10000).default(1),
|
|
41
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
42
|
+
})
|
|
43
|
+
.strict(),
|
|
44
|
+
async handler(raw) {
|
|
45
|
+
requireScope('strapi:media:read');
|
|
46
|
+
const schema = this.inputSchema as z.ZodTypeAny;
|
|
47
|
+
const { page, pageSize } = schema.parse(raw) as { page: number; pageSize: number };
|
|
48
|
+
const result = await strapi.db.query('plugin::upload.file').findPage({
|
|
49
|
+
page,
|
|
50
|
+
pageSize,
|
|
51
|
+
orderBy: { id: 'desc' },
|
|
52
|
+
});
|
|
53
|
+
const files = result.results.map((f: Record<string, unknown>) => ({
|
|
54
|
+
id: f.id,
|
|
55
|
+
name: f.name,
|
|
56
|
+
url: f.url,
|
|
57
|
+
mime: f.mime,
|
|
58
|
+
size: f.size,
|
|
59
|
+
hash: f.hash,
|
|
60
|
+
createdAt: f.createdAt,
|
|
61
|
+
}));
|
|
62
|
+
return json({ page, pageSize, count: files.length, total: result.pagination?.total, files });
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
name: 'strapi.media.upload',
|
|
68
|
+
description:
|
|
69
|
+
'Upload a single file via base64 or remote URL. Subject to MIME allowlist and size cap.',
|
|
70
|
+
scope: 'strapi:media:write',
|
|
71
|
+
inputSchema: z
|
|
72
|
+
.object({
|
|
73
|
+
filename: z.string().regex(FILENAME_RE, 'invalid filename'),
|
|
74
|
+
mime: z.string().regex(MIME_RE, 'invalid mime'),
|
|
75
|
+
source: z.union([
|
|
76
|
+
z.object({ base64: z.string().min(1).max(MAX_BASE64_LEN) }),
|
|
77
|
+
z.object({ url: z.string().url() }),
|
|
78
|
+
]),
|
|
79
|
+
})
|
|
80
|
+
.strict(),
|
|
81
|
+
async handler(raw) {
|
|
82
|
+
requireScope('strapi:media:write');
|
|
83
|
+
const schema = this.inputSchema as z.ZodTypeAny;
|
|
84
|
+
const input = schema.parse(raw) as {
|
|
85
|
+
filename: string;
|
|
86
|
+
mime: string;
|
|
87
|
+
source: { base64: string } | { url: string };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const mime = input.mime.toLowerCase();
|
|
91
|
+
if (!cfg.upload.mimeAllowlist.includes(mime)) {
|
|
92
|
+
throw badRequest(`mime not allowed: ${mime}`);
|
|
93
|
+
}
|
|
94
|
+
if (mime === 'image/svg+xml' && !cfg.upload.allowSvg) {
|
|
95
|
+
throw badRequest('SVG uploads disabled');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let buf: Buffer;
|
|
99
|
+
if ('base64' in input.source) {
|
|
100
|
+
try {
|
|
101
|
+
buf = Buffer.from(input.source.base64, 'base64');
|
|
102
|
+
} catch {
|
|
103
|
+
throw badRequest('invalid base64');
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
buf = await fetchBounded(input.source.url, cfg.upload.maxBytes);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (buf.byteLength === 0) throw badRequest('empty file');
|
|
110
|
+
if (buf.byteLength > cfg.upload.maxBytes) {
|
|
111
|
+
throw badRequest(`file too large (max ${cfg.upload.maxBytes} bytes)`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const dir = await mkdtemp(join(tmpdir(), 'mcp-upload-'));
|
|
115
|
+
const path = join(dir, input.filename);
|
|
116
|
+
try {
|
|
117
|
+
await writeFile(path, buf);
|
|
118
|
+
const uploadSvc = strapi.plugin('upload').service('upload');
|
|
119
|
+
const [file] = (await uploadSvc.upload({
|
|
120
|
+
data: { fileInfo: { name: input.filename } },
|
|
121
|
+
files: {
|
|
122
|
+
filepath: path,
|
|
123
|
+
originalFilename: input.filename,
|
|
124
|
+
mimetype: mime,
|
|
125
|
+
size: buf.byteLength,
|
|
126
|
+
},
|
|
127
|
+
})) as Array<Record<string, unknown>>;
|
|
128
|
+
return json({
|
|
129
|
+
id: file.id,
|
|
130
|
+
name: file.name,
|
|
131
|
+
url: file.url,
|
|
132
|
+
mime: file.mime,
|
|
133
|
+
size: file.size,
|
|
134
|
+
});
|
|
135
|
+
} finally {
|
|
136
|
+
await rm(dir, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function badRequest(message: string): Error {
|
|
144
|
+
const err = new Error(message);
|
|
145
|
+
(err as Error & { code?: string }).code = 'bad_request';
|
|
146
|
+
return err;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function fetchBounded(url: string, maxBytes: number): Promise<Buffer> {
|
|
150
|
+
const parsed = new URL(url);
|
|
151
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
152
|
+
throw badRequest('only http(s) URLs supported');
|
|
153
|
+
}
|
|
154
|
+
const resp = await request(url, {
|
|
155
|
+
method: 'GET',
|
|
156
|
+
maxRedirections: 3,
|
|
157
|
+
headersTimeout: 10_000,
|
|
158
|
+
bodyTimeout: 30_000,
|
|
159
|
+
});
|
|
160
|
+
if (resp.statusCode >= 400) throw badRequest(`remote returned ${resp.statusCode}`);
|
|
161
|
+
const chunks: Buffer[] = [];
|
|
162
|
+
let total = 0;
|
|
163
|
+
for await (const chunk of resp.body) {
|
|
164
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);
|
|
165
|
+
total += b.byteLength;
|
|
166
|
+
if (total > maxBytes) throw badRequest(`remote file exceeds ${maxBytes} bytes`);
|
|
167
|
+
chunks.push(b);
|
|
168
|
+
}
|
|
169
|
+
return Buffer.concat(chunks);
|
|
170
|
+
}
|