@snokam/mcp-api 0.5.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/dist/auth.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Token acquisition for Snokam backend APIs.
3
+ *
4
+ * Supports three modes (auto-detected):
5
+ *
6
+ * 1. **OBO (On-Behalf-Of):** When SNOKAM_USER_JWT is set, exchanges the user
7
+ * JWT for a service-specific token via Azure AD OBO flow.
8
+ * Requires AZURE_AD_CLIENT_ID, AZURE_AD_TENANT_ID, and either
9
+ * AZURE_AD_SECRET (client secret) or managed identity.
10
+ *
11
+ * 2. **DefaultAzureCredential:** When no user JWT is present, falls back to
12
+ * Azure CLI (local dev), managed identity (Azure), etc.
13
+ *
14
+ * 3. **No auth:** Endpoints without a scope get no Authorization header.
15
+ */
16
+ export declare function getAccessToken(scope: string | null): Promise<string | null>;
package/dist/auth.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Token acquisition for Snokam backend APIs.
3
+ *
4
+ * Supports three modes (auto-detected):
5
+ *
6
+ * 1. **OBO (On-Behalf-Of):** When SNOKAM_USER_JWT is set, exchanges the user
7
+ * JWT for a service-specific token via Azure AD OBO flow.
8
+ * Requires AZURE_AD_CLIENT_ID, AZURE_AD_TENANT_ID, and either
9
+ * AZURE_AD_SECRET (client secret) or managed identity.
10
+ *
11
+ * 2. **DefaultAzureCredential:** When no user JWT is present, falls back to
12
+ * Azure CLI (local dev), managed identity (Azure), etc.
13
+ *
14
+ * 3. **No auth:** Endpoints without a scope get no Authorization header.
15
+ */
16
+ import { DefaultAzureCredential, OnBehalfOfCredential, } from "@azure/identity";
17
+ // Cache credentials per scope to avoid re-creating them
18
+ const credentialCache = new Map();
19
+ function getOboCredential(userJwt, clientId, tenantId) {
20
+ const clientSecret = process.env.AZURE_AD_SECRET;
21
+ if (clientSecret) {
22
+ return new OnBehalfOfCredential({
23
+ tenantId,
24
+ clientId,
25
+ clientSecret,
26
+ userAssertionToken: userJwt,
27
+ });
28
+ }
29
+ // Managed identity as client assertion (federated identity)
30
+ const mi = new DefaultAzureCredential();
31
+ return new OnBehalfOfCredential({
32
+ tenantId,
33
+ clientId,
34
+ userAssertionToken: userJwt,
35
+ getAssertion: async () => {
36
+ const token = await mi.getToken("api://AzureADTokenExchange");
37
+ return token.token;
38
+ },
39
+ });
40
+ }
41
+ export async function getAccessToken(scope) {
42
+ if (!scope)
43
+ return null;
44
+ const userJwt = process.env.SNOKAM_USER_JWT;
45
+ const clientId = process.env.AZURE_AD_CLIENT_ID ?? "";
46
+ const tenantId = process.env.AZURE_AD_TENANT_ID ?? "";
47
+ let credential;
48
+ if (userJwt && clientId && tenantId) {
49
+ // OBO mode
50
+ const cacheKey = `obo:${scope}`;
51
+ if (!credentialCache.has(cacheKey)) {
52
+ credentialCache.set(cacheKey, getOboCredential(userJwt, clientId, tenantId));
53
+ }
54
+ credential = credentialCache.get(cacheKey);
55
+ }
56
+ else {
57
+ // Default credential (Azure CLI locally, managed identity in Azure)
58
+ const cacheKey = "default";
59
+ if (!credentialCache.has(cacheKey)) {
60
+ credentialCache.set(cacheKey, new DefaultAzureCredential());
61
+ }
62
+ credential = credentialCache.get(cacheKey);
63
+ }
64
+ try {
65
+ const token = await credential.getToken(scope);
66
+ return token?.token ?? null;
67
+ }
68
+ catch (error) {
69
+ console.error(`[snokam-mcp] Failed to get token for scope ${scope}:`, error instanceof Error ? error.message : error);
70
+ return null;
71
+ }
72
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Snokam MCP Server
4
+ *
5
+ * Exposes Snokam backend APIs as MCP tools by reading bundled OpenAPI specs.
6
+ * Auth is handled via @azure/identity — supports Azure CLI (local),
7
+ * managed identity (Azure), and OBO (when SNOKAM_USER_JWT is set).
8
+ */
9
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Snokam MCP Server
4
+ *
5
+ * Exposes Snokam backend APIs as MCP tools by reading bundled OpenAPI specs.
6
+ * Auth is handled via @azure/identity — supports Azure CLI (local),
7
+ * managed identity (Azure), and OBO (when SNOKAM_USER_JWT is set).
8
+ */
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
12
+ import { fetchSpecs } from "./openapi-loader.js";
13
+ import { getAccessToken } from "./auth.js";
14
+ // ---------------------------------------------------------------------------
15
+ // Config
16
+ // ---------------------------------------------------------------------------
17
+ const ENVIRONMENT = process.env.SNOKAM_ENVIRONMENT ?? "production";
18
+ // ---------------------------------------------------------------------------
19
+ // JSON Schema builder for tool inputs
20
+ // ---------------------------------------------------------------------------
21
+ function buildInputSchema(endpoint) {
22
+ const properties = {};
23
+ const required = [];
24
+ for (const param of endpoint.parameters) {
25
+ const prop = {};
26
+ if (param.schema?.type)
27
+ prop.type = param.schema.type;
28
+ if (param.schema?.enum)
29
+ prop.enum = param.schema.enum;
30
+ if (param.schema?.format)
31
+ prop.format = param.schema.format;
32
+ if (param.schema?.items)
33
+ prop.items = param.schema.items;
34
+ if (param.description)
35
+ prop.description = param.description;
36
+ if (!prop.type)
37
+ prop.type = "string";
38
+ properties[param.name] = prop;
39
+ if (param.required)
40
+ required.push(param.name);
41
+ }
42
+ if (endpoint.requestBody) {
43
+ properties.body = {
44
+ type: "object",
45
+ description: endpoint.requestBody.description ?? "Request body",
46
+ };
47
+ // Extract schema from content type if available
48
+ const jsonContent = endpoint.requestBody.content?.["application/json"];
49
+ if (jsonContent?.schema) {
50
+ properties.body = {
51
+ ...properties.body,
52
+ ...jsonContent.schema,
53
+ };
54
+ }
55
+ if (endpoint.requestBody.required)
56
+ required.push("body");
57
+ }
58
+ return {
59
+ type: "object",
60
+ properties,
61
+ required: required.length > 0 ? required : undefined,
62
+ };
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // HTTP call execution
66
+ // ---------------------------------------------------------------------------
67
+ async function executeCall(endpoint, args) {
68
+ let url = `${endpoint.baseUrl}${endpoint.path}`;
69
+ const queryParams = [];
70
+ for (const param of endpoint.parameters) {
71
+ const value = args[param.name];
72
+ if (value === undefined)
73
+ continue;
74
+ if (param.in === "path") {
75
+ url = url.replace(`{${param.name}}`, encodeURIComponent(String(value)));
76
+ }
77
+ else if (param.in === "query") {
78
+ queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(String(value))}`);
79
+ }
80
+ }
81
+ if (queryParams.length > 0) {
82
+ url += `?${queryParams.join("&")}`;
83
+ }
84
+ const headers = {
85
+ Accept: "application/json",
86
+ };
87
+ const token = await getAccessToken(endpoint.scope);
88
+ if (token) {
89
+ headers.Authorization = `Bearer ${token}`;
90
+ }
91
+ let fetchBody;
92
+ if (args.body !== undefined && endpoint.method !== "GET") {
93
+ headers["Content-Type"] = "application/json";
94
+ fetchBody = JSON.stringify(args.body);
95
+ }
96
+ const response = await fetch(url, {
97
+ method: endpoint.method,
98
+ headers,
99
+ body: fetchBody,
100
+ });
101
+ let body;
102
+ const contentType = response.headers.get("content-type") ?? "";
103
+ if (contentType.includes("application/json")) {
104
+ body = await response.json();
105
+ }
106
+ else {
107
+ body = await response.text();
108
+ }
109
+ return { status: response.status, body };
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // Server setup
113
+ // ---------------------------------------------------------------------------
114
+ async function main() {
115
+ const endpoints = await fetchSpecs(ENVIRONMENT);
116
+ if (endpoints.length === 0) {
117
+ console.error("[snokam-mcp] No endpoints loaded. Ensure specs/*.json files exist.");
118
+ }
119
+ const endpointsByTool = new Map();
120
+ for (const ep of endpoints) {
121
+ endpointsByTool.set(ep.toolName, ep);
122
+ }
123
+ const server = new Server({
124
+ name: "snokam",
125
+ version: "0.2.0",
126
+ }, {
127
+ capabilities: {
128
+ tools: {},
129
+ resources: {},
130
+ },
131
+ });
132
+ // List resources
133
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
134
+ resources: [
135
+ {
136
+ uri: "snokam://about",
137
+ name: "About Snøkam",
138
+ description: "Information about Snøkam and available API services",
139
+ mimeType: "text/markdown",
140
+ },
141
+ ],
142
+ }));
143
+ // Read resource
144
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
145
+ const { uri } = request.params;
146
+ if (uri === "snokam://about") {
147
+ // Group endpoints by service with their descriptions
148
+ const serviceMap = new Map();
149
+ for (const ep of endpoints) {
150
+ const existing = serviceMap.get(ep.service);
151
+ if (existing) {
152
+ existing.count++;
153
+ }
154
+ else {
155
+ serviceMap.set(ep.service, {
156
+ count: 1,
157
+ description: ep.serviceDescription,
158
+ });
159
+ }
160
+ }
161
+ const serviceList = Array.from(serviceMap.entries())
162
+ .sort(([a], [b]) => a.localeCompare(b))
163
+ .map(([service, { count, description }]) => {
164
+ return `- **${service}** (${count} endpoints): ${description}`;
165
+ })
166
+ .join("\n");
167
+ const about = `# Snøkam MCP Server
168
+
169
+ **Snøkam** is a Norwegian software consulting company. This MCP server provides programmatic access to Snøkam's internal backend APIs.
170
+
171
+ ## Environment
172
+
173
+ Currently connected to: **${ENVIRONMENT}**
174
+
175
+ ## Available Services
176
+
177
+ ${serviceList}
178
+
179
+ ## Authentication
180
+
181
+ - **Public endpoints**: No authentication required (e.g., \`employees__GetEmployeesPublic\`)
182
+ - **Protected endpoints**: Require Azure AD authentication (e.g., \`employees__GetEmployeesProtected\`)
183
+
184
+ ## Common Use Cases
185
+
186
+ **Find out who works at Snøkam:**
187
+ \`\`\`
188
+ Use: employees__GetEmployeesPublic
189
+ Returns: List of all employees with names, roles, and technologies
190
+ \`\`\`
191
+
192
+ **Get upcoming events:**
193
+ \`\`\`
194
+ Use: events__GetPublicEvents (if available)
195
+ Returns: Company events and gatherings
196
+ \`\`\`
197
+
198
+ **Check office status or control music:**
199
+ \`\`\`
200
+ Use: office__* tools
201
+ Controls: Sonos speakers, lights, YouTube queue
202
+ \`\`\`
203
+ `;
204
+ return {
205
+ contents: [
206
+ {
207
+ uri,
208
+ mimeType: "text/markdown",
209
+ text: about,
210
+ },
211
+ ],
212
+ };
213
+ }
214
+ return {
215
+ contents: [],
216
+ isError: true,
217
+ };
218
+ });
219
+ // List tools
220
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
221
+ tools: endpoints.map((ep) => ({
222
+ name: ep.toolName,
223
+ description: ep.description || ep.summary || `${ep.method} ${ep.path}`,
224
+ inputSchema: buildInputSchema(ep),
225
+ })),
226
+ }));
227
+ // Call tool
228
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
229
+ const { name, arguments: args = {} } = request.params;
230
+ const endpoint = endpointsByTool.get(name);
231
+ if (!endpoint) {
232
+ return {
233
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
234
+ isError: true,
235
+ };
236
+ }
237
+ try {
238
+ const result = await executeCall(endpoint, args);
239
+ const text = typeof result.body === "string"
240
+ ? result.body
241
+ : JSON.stringify(result.body, null, 2);
242
+ return {
243
+ content: [
244
+ {
245
+ type: "text",
246
+ text: `HTTP ${result.status}\n\n${text}`,
247
+ },
248
+ ],
249
+ isError: result.status >= 400,
250
+ };
251
+ }
252
+ catch (error) {
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: `Request failed: ${error instanceof Error ? error.message : String(error)}`,
258
+ },
259
+ ],
260
+ isError: true,
261
+ };
262
+ }
263
+ });
264
+ const transport = new StdioServerTransport();
265
+ await server.connect(transport);
266
+ console.error("[snokam-mcp] Server running on stdio");
267
+ }
268
+ main().catch((error) => {
269
+ console.error("[snokam-mcp] Fatal error:", error);
270
+ process.exit(1);
271
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
+ *
4
+ * Each backend function is available at `{service}.api.snokam.no` (prod)
5
+ * or `{service}.api.test.snokam.no` (test). The loader fetches `/swagger.json`
6
+ * from each, extracts endpoints, parameters, and OAuth scopes to generate
7
+ * MCP-compatible tool metadata.
8
+ */
9
+ export interface ApiEndpoint {
10
+ service: string;
11
+ serviceDescription: string;
12
+ toolName: string;
13
+ operationId: string;
14
+ method: string;
15
+ path: string;
16
+ baseUrl: string;
17
+ summary: string;
18
+ description: string;
19
+ parameters: OpenApiParameter[];
20
+ requestBody: OpenApiRequestBody | null;
21
+ /** OAuth2 scope in `.default` format for OBO exchange, or null for public endpoints. */
22
+ scope: string | null;
23
+ }
24
+ interface OpenApiParameter {
25
+ name: string;
26
+ in: "query" | "path" | "header";
27
+ description?: string;
28
+ required?: boolean;
29
+ schema?: {
30
+ type?: string;
31
+ format?: string;
32
+ enum?: string[];
33
+ items?: {
34
+ type?: string;
35
+ };
36
+ };
37
+ }
38
+ interface OpenApiRequestBody {
39
+ description?: string;
40
+ required?: boolean;
41
+ content?: Record<string, {
42
+ schema?: Record<string, unknown>;
43
+ }>;
44
+ }
45
+ export declare function fetchSpecs(environment: string): Promise<ApiEndpoint[]>;
46
+ export {};
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Fetch OpenAPI specs from live Snokam APIs and produce tool definitions.
3
+ *
4
+ * Each backend function is available at `{service}.api.snokam.no` (prod)
5
+ * or `{service}.api.test.snokam.no` (test). The loader fetches `/swagger.json`
6
+ * from each, extracts endpoints, parameters, and OAuth scopes to generate
7
+ * MCP-compatible tool metadata.
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Service discovery - reads from bundled specs directory at runtime
11
+ // ---------------------------------------------------------------------------
12
+ async function discoverServices(environment) {
13
+ try {
14
+ const { readdir } = await import("fs/promises");
15
+ const { fileURLToPath } = await import("url");
16
+ const { dirname, join } = await import("path");
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const specsDir = join(__dirname, "..", "specs", environment);
19
+ const files = await readdir(specsDir);
20
+ return files
21
+ .filter((f) => f.endsWith(".json"))
22
+ .map((f) => f.replace(".json", ""))
23
+ .sort();
24
+ }
25
+ catch {
26
+ // No bundled specs found - return empty array
27
+ return [];
28
+ }
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // Environment-aware URL resolution
32
+ // ---------------------------------------------------------------------------
33
+ const PROD_DOMAIN = "api.snokam.no";
34
+ const TEST_DOMAIN = "api.test.snokam.no";
35
+ function getBaseDomain(environment) {
36
+ return environment === "test" ? TEST_DOMAIN : PROD_DOMAIN;
37
+ }
38
+ function getBaseUrl(service, environment) {
39
+ return `https://${service}.${getBaseDomain(environment)}`;
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // Scope extraction
43
+ // ---------------------------------------------------------------------------
44
+ function extractScope(operation) {
45
+ for (const secReq of operation.security ?? []) {
46
+ for (const scopes of Object.values(secReq)) {
47
+ if (!Array.isArray(scopes))
48
+ continue;
49
+ for (const s of scopes) {
50
+ if (s.startsWith("api://")) {
51
+ const base = s.substring(0, s.lastIndexOf("/"));
52
+ return `${base}/.default`;
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Tool naming
61
+ // ---------------------------------------------------------------------------
62
+ function makeToolName(service, operationId) {
63
+ return `${service}__${operationId}`;
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Spec parsing
67
+ // ---------------------------------------------------------------------------
68
+ function parseSpec(spec, service, baseUrl) {
69
+ const endpoints = [];
70
+ const paths = spec.paths;
71
+ if (!paths || Object.keys(paths).length === 0)
72
+ return endpoints;
73
+ const serviceDescription = spec.info.description || spec.info.title || "";
74
+ for (const [path, pathItem] of Object.entries(paths)) {
75
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
76
+ const operation = pathItem[method];
77
+ if (!operation)
78
+ continue;
79
+ const operationId = operation.operationId ?? `${method}_${path.replace(/\//g, "_")}`;
80
+ const summary = operation.summary ?? "";
81
+ const description = operation.description ?? summary;
82
+ const scope = extractScope(operation);
83
+ endpoints.push({
84
+ service,
85
+ serviceDescription,
86
+ toolName: makeToolName(service, operationId),
87
+ operationId,
88
+ method: method.toUpperCase(),
89
+ path,
90
+ baseUrl,
91
+ summary,
92
+ description,
93
+ parameters: operation.parameters ?? [],
94
+ requestBody: operation.requestBody ?? null,
95
+ scope,
96
+ });
97
+ }
98
+ }
99
+ return endpoints;
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Public API
103
+ // ---------------------------------------------------------------------------
104
+ export async function fetchSpecs(environment) {
105
+ // Try to load bundled specs first (for speed)
106
+ try {
107
+ const bundledSpecs = await loadBundledSpecs(environment);
108
+ if (bundledSpecs.length > 0) {
109
+ console.error(`[snokam-mcp] Loaded ${bundledSpecs.length} endpoints from bundled specs (env=${environment})`);
110
+ return bundledSpecs;
111
+ }
112
+ }
113
+ catch (error) {
114
+ console.error(`[snokam-mcp] Failed to load bundled specs, falling back to live fetch:`, error);
115
+ }
116
+ // Fallback to live fetching (slower, for development)
117
+ // Discover services from environment to know which APIs to fetch
118
+ const services = await discoverServices(environment);
119
+ if (services.length === 0) {
120
+ console.error("[snokam-mcp] No services discovered. Unable to fetch specs.");
121
+ return [];
122
+ }
123
+ const endpoints = [];
124
+ const results = await Promise.allSettled(services.map(async (service) => {
125
+ const baseUrl = getBaseUrl(service, environment);
126
+ const swaggerUrl = `${baseUrl}/swagger.json`;
127
+ const response = await fetch(swaggerUrl, {
128
+ signal: AbortSignal.timeout(10_000),
129
+ });
130
+ if (!response.ok) {
131
+ throw new Error(`HTTP ${response.status}`);
132
+ }
133
+ const spec = (await response.json());
134
+ return { service, baseUrl, spec };
135
+ }));
136
+ let successCount = 0;
137
+ for (const result of results) {
138
+ if (result.status === "fulfilled") {
139
+ const { service, baseUrl, spec } = result.value;
140
+ endpoints.push(...parseSpec(spec, service, baseUrl));
141
+ successCount++;
142
+ }
143
+ else {
144
+ const idx = results.indexOf(result);
145
+ console.error(`[snokam-mcp] Failed to fetch ${services[idx]}: ${result.reason}`);
146
+ }
147
+ }
148
+ console.error(`[snokam-mcp] Loaded ${endpoints.length} endpoints from ${successCount}/${services.length} services (env=${environment})`);
149
+ return endpoints;
150
+ }
151
+ async function loadBundledSpecs(environment) {
152
+ const { readFile } = await import("fs/promises");
153
+ const { fileURLToPath } = await import("url");
154
+ const { dirname, join } = await import("path");
155
+ const __dirname = dirname(fileURLToPath(import.meta.url));
156
+ const specsDir = join(__dirname, "..", "specs", environment);
157
+ // Discover which specs are bundled
158
+ const services = await discoverServices(environment);
159
+ const endpoints = [];
160
+ for (const service of services) {
161
+ try {
162
+ const specPath = join(specsDir, `${service}.json`);
163
+ const specData = await readFile(specPath, "utf-8");
164
+ const spec = JSON.parse(specData);
165
+ const baseUrl = getBaseUrl(service, environment);
166
+ endpoints.push(...parseSpec(spec, service, baseUrl));
167
+ }
168
+ catch (error) {
169
+ // Spec file doesn't exist or is invalid, skip
170
+ continue;
171
+ }
172
+ }
173
+ return endpoints;
174
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@snokam/mcp-api",
3
+ "version": "0.5.1",
4
+ "description": "MCP server exposing Snokam backend APIs as tools for Claude Code and other MCP clients",
5
+ "type": "module",
6
+ "bin": {
7
+ "snokam-mcp": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "scripts": {
11
+ "bundle-specs": "node scripts/bundle-specs.js",
12
+ "bundle-specs:prod": "node scripts/bundle-specs.js production",
13
+ "bundle-specs:test": "node scripts/bundle-specs.js test",
14
+ "prebuild": "npm run bundle-specs:prod && npm run bundle-specs:test",
15
+ "build": "tsc",
16
+ "dev": "tsc --watch",
17
+ "start": "node dist/index.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "specs"
22
+ ],
23
+ "dependencies": {
24
+ "@azure/identity": "^4.6.0",
25
+ "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "zod": "^3.24.4"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.15.0",
30
+ "typescript": "~5.8.3"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }