opplevagent-mcp 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/index.js +300 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Fredriksen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Opplevagent MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server for **[Opplevagent](https://opplevagent.no)** — discover Norwegian experiences and activities from Claude Desktop, Cursor, or any MCP client.
|
|
4
|
+
|
|
5
|
+
Search tours, courses and things to do across Norway and filter by **county, municipality, category, weather, season, group size, age and price**. Every result links straight to a booking page.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/opplevagent-mcp)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
### Claude Desktop
|
|
12
|
+
|
|
13
|
+
Add to `~/.claude/claude_desktop_config.json` (macOS/Linux) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"opplevagent": {
|
|
19
|
+
"command": "npx",
|
|
20
|
+
"args": ["opplevagent-mcp"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Restart Claude Desktop. No API key required.
|
|
27
|
+
|
|
28
|
+
### Run directly
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx opplevagent-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `opplev_discover` | Search experiences by structured filters: `fylke` (county), `kommune` (municipality), `category`, `indoor_outdoor`, `weather`, `season`, `group_size`, `age`, `max_price`, `duration_max`, `language`, `limit`. Returns ranked experiences with place, category, duration, price and a booking link. |
|
|
39
|
+
| `opplev_info` | Get the full profile for one experience by `id` (UUID) — description, place, season, duration, group size, age suitability, price, languages, accessibility, meeting point and booking link. |
|
|
40
|
+
| `opplev_categories` | List all experience categories with the count of published experiences in each. |
|
|
41
|
+
|
|
42
|
+
All tools are **read-only** and query the public Opplevagent API. No authentication.
|
|
43
|
+
|
|
44
|
+
## Example prompts
|
|
45
|
+
|
|
46
|
+
- *"Hva kan vi finne på i Oslo når det regner?"*
|
|
47
|
+
- *"Hvalsafari i Tromsø"*
|
|
48
|
+
- *"Familievennlige aktiviteter utendørs i Bergen om sommeren"*
|
|
49
|
+
- *"Things to do in Trondheim under 500 kr"*
|
|
50
|
+
|
|
51
|
+
## Categories
|
|
52
|
+
|
|
53
|
+
`kultur_historie` · `sightseeing_transport` · `adrenalin_action` · `dyreliv_safari` · `vinter_sno` · `natur_friluft` · `overnatting_opplevelse` · `velvaere_spa` · `mat_drikke`
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
| Env var | Default | Purpose |
|
|
58
|
+
|---------|---------|---------|
|
|
59
|
+
| `OPPLEVAGENT_URL` | `https://opplevagent.no` | Base URL of the Opplevagent API. Override to point at a staging instance. |
|
|
60
|
+
|
|
61
|
+
## How it works
|
|
62
|
+
|
|
63
|
+
Opplevagent is an **A2A (Agent-to-Agent) marketplace** for Norwegian experiences. This MCP server is a thin, read-only wrapper over the public discovery API:
|
|
64
|
+
|
|
65
|
+
- REST: `https://opplevagent.no/api/opplevelser`
|
|
66
|
+
- Discover: `https://opplevagent.no/api/opplevelser/discover`
|
|
67
|
+
- Agent card: `https://opplevagent.no/.well-known/agent-card.json`
|
|
68
|
+
- OpenAPI: `https://opplevagent.no/openapi.json`
|
|
69
|
+
- Remote MCP (Streamable HTTP): `https://opplevagent.no/mcp`
|
|
70
|
+
|
|
71
|
+
## Links
|
|
72
|
+
|
|
73
|
+
- Website: <https://opplevagent.no>
|
|
74
|
+
- npm: <https://www.npmjs.com/package/opplevagent-mcp>
|
|
75
|
+
- Issues: <https://github.com/slookisen/opplevagent-mcp/issues>
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT © Daniel Fredriksen
|
package/index.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Opplevagent MCP Server — Discover Norwegian experiences & activities via Claude Desktop
|
|
5
|
+
*
|
|
6
|
+
* Install:
|
|
7
|
+
* npx opplevagent-mcp
|
|
8
|
+
*
|
|
9
|
+
* Or add to Claude Desktop config (~/.claude/claude_desktop_config.json):
|
|
10
|
+
* {
|
|
11
|
+
* "mcpServers": {
|
|
12
|
+
* "opplevagent": {
|
|
13
|
+
* "command": "npx",
|
|
14
|
+
* "args": ["opplevagent-mcp"]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Backed by the public Opplevagent A2A marketplace: https://opplevagent.no
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
const BASE_URL = process.env.OPPLEVAGENT_URL || "https://opplevagent.no";
|
|
27
|
+
const UA = "opplevagent-mcp/0.1.0";
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
async function fetchJSON(url) {
|
|
32
|
+
const res = await fetch(url, {
|
|
33
|
+
headers: { Accept: "application/json", "User-Agent": UA },
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Norwegian labels for the fixed category vocabulary.
|
|
40
|
+
const CATEGORY_LABELS = {
|
|
41
|
+
kultur_historie: "Kultur & historie",
|
|
42
|
+
sightseeing_transport: "Sightseeing & transport",
|
|
43
|
+
adrenalin_action: "Adrenalin & action",
|
|
44
|
+
dyreliv_safari: "Dyreliv & safari",
|
|
45
|
+
vinter_sno: "Vinter & snø",
|
|
46
|
+
natur_friluft: "Natur & friluft",
|
|
47
|
+
overnatting_opplevelse: "Overnatting & opplevelse",
|
|
48
|
+
velvaere_spa: "Velvære & spa",
|
|
49
|
+
mat_drikke: "Mat & drikke",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function categoryLabel(c) {
|
|
53
|
+
return CATEGORY_LABELS[c] || c;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function profileUrl(slug) {
|
|
57
|
+
return slug ? `${BASE_URL}/opplevelse/${slug}` : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function priceText(exp) {
|
|
61
|
+
if (exp.price_from != null) {
|
|
62
|
+
const unit = exp.price_unit ? ` ${exp.price_unit}` : "";
|
|
63
|
+
return `fra ${exp.price_from} kr${unit}`;
|
|
64
|
+
}
|
|
65
|
+
if (exp.price_band) return String(exp.price_band);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function durationText(exp) {
|
|
70
|
+
const d = exp.duration_min ?? exp.duration_max;
|
|
71
|
+
if (d == null) return null;
|
|
72
|
+
if (d >= 1440) return `${Math.round((d / 1440) * 10) / 10} dager`;
|
|
73
|
+
if (d >= 60) return `${Math.round((d / 60) * 10) / 10} t`;
|
|
74
|
+
return `${d} min`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Format one experience from the /discover result list.
|
|
78
|
+
function formatExperience(exp, idx) {
|
|
79
|
+
const lines = [`**${idx}. ${exp.title}**`];
|
|
80
|
+
|
|
81
|
+
const meta = [];
|
|
82
|
+
const place = [exp.kommune, exp.fylke].filter(Boolean).join(", ");
|
|
83
|
+
if (place) meta.push(`📍 ${place}`);
|
|
84
|
+
if (exp.category) meta.push(`🏷️ ${categoryLabel(exp.category)}`);
|
|
85
|
+
if (exp.indoor_outdoor) {
|
|
86
|
+
const io = { indoor: "innendørs", outdoor: "utendørs", both: "inne/ute" }[exp.indoor_outdoor] || exp.indoor_outdoor;
|
|
87
|
+
meta.push(io);
|
|
88
|
+
}
|
|
89
|
+
const dur = durationText(exp);
|
|
90
|
+
if (dur) meta.push(`⏱ ${dur}`);
|
|
91
|
+
const price = priceText(exp);
|
|
92
|
+
if (price) meta.push(`💰 ${price}`);
|
|
93
|
+
if (meta.length) lines.push(` ${meta.join(" · ")}`);
|
|
94
|
+
|
|
95
|
+
const links = [];
|
|
96
|
+
const url = profileUrl(exp.slug);
|
|
97
|
+
if (url) links.push(`🔗 ${url}`);
|
|
98
|
+
if (exp.booking_url) links.push(`🎟️ Book: ${exp.booking_url}`);
|
|
99
|
+
if (links.length) lines.push(` ${links.join(" · ")}`);
|
|
100
|
+
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── MCP Server ───────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const server = new McpServer({
|
|
107
|
+
name: "opplevagent",
|
|
108
|
+
version: "0.1.0",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Tool 1: Discover experiences by structured filters
|
|
112
|
+
// Annotations: read-only, safe, idempotent, open-world (queries a public registry).
|
|
113
|
+
server.registerTool(
|
|
114
|
+
"opplev_discover",
|
|
115
|
+
{
|
|
116
|
+
title: "Discover Norwegian experiences",
|
|
117
|
+
description:
|
|
118
|
+
"Search Norwegian experiences and activities (tours, courses, things to do) by structured filters. " +
|
|
119
|
+
"Use this when a user asks what to do somewhere — e.g. 'hva kan vi finne på i Oslo når det regner', " +
|
|
120
|
+
"'family-friendly outdoor activities in Tromsø in winter', 'things to do in Trondheim under 500 kr'. " +
|
|
121
|
+
"Translate the user's natural-language request into the filters below. Returns ranked experiences with " +
|
|
122
|
+
"place, category, duration, price and a booking link.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
fylke: z.string().optional().describe("County / region, e.g. 'Oslo', 'Troms', 'Vestland'"),
|
|
125
|
+
kommune: z.string().optional().describe("Municipality, e.g. 'Tromsø', 'Bergen'"),
|
|
126
|
+
category: z
|
|
127
|
+
.enum([
|
|
128
|
+
"kultur_historie",
|
|
129
|
+
"sightseeing_transport",
|
|
130
|
+
"adrenalin_action",
|
|
131
|
+
"dyreliv_safari",
|
|
132
|
+
"vinter_sno",
|
|
133
|
+
"natur_friluft",
|
|
134
|
+
"overnatting_opplevelse",
|
|
135
|
+
"velvaere_spa",
|
|
136
|
+
"mat_drikke",
|
|
137
|
+
])
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Experience category. Use opplev_categories to list valid values with counts."),
|
|
140
|
+
indoor_outdoor: z.enum(["indoor", "outdoor", "both"]).optional().describe("Indoor, outdoor, or both"),
|
|
141
|
+
weather: z
|
|
142
|
+
.enum(["rain", "snow", "clear", "any"])
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Current/expected weather. 'rain' and 'snow' prefer indoor & weather-independent activities."),
|
|
145
|
+
season: z.enum(["spring", "summer", "autumn", "winter"]).optional().describe("Season"),
|
|
146
|
+
group_size: z.number().int().optional().describe("Number of people in the group"),
|
|
147
|
+
age: z.number().int().optional().describe("Age of participant (for age-suitability filtering)"),
|
|
148
|
+
max_price: z.number().optional().describe("Maximum price per person in NOK"),
|
|
149
|
+
duration_max: z.number().int().optional().describe("Maximum duration in minutes"),
|
|
150
|
+
language: z.string().optional().describe("Preferred language for the experience, e.g. 'en', 'no'"),
|
|
151
|
+
limit: z.number().min(1).max(50).default(10).describe("Max results"),
|
|
152
|
+
},
|
|
153
|
+
annotations: {
|
|
154
|
+
title: "Discover Norwegian experiences",
|
|
155
|
+
readOnlyHint: true,
|
|
156
|
+
destructiveHint: false,
|
|
157
|
+
idempotentHint: true,
|
|
158
|
+
openWorldHint: true,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async (args) => {
|
|
162
|
+
const params = new URLSearchParams();
|
|
163
|
+
for (const [k, v] of Object.entries(args)) {
|
|
164
|
+
if (v !== undefined && v !== null && v !== "") params.set(k, String(v));
|
|
165
|
+
}
|
|
166
|
+
if (!params.has("limit")) params.set("limit", "10");
|
|
167
|
+
|
|
168
|
+
const data = await fetchJSON(`${BASE_URL}/api/opplevelser/discover?${params}`);
|
|
169
|
+
|
|
170
|
+
if (!data.results?.length) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{ type: "text", text: "Ingen opplevelser funnet med disse filtrene. Prøv et bredere søk (færre filtre)." },
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const applied = Object.entries(data.query || {})
|
|
179
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
180
|
+
.join(", ");
|
|
181
|
+
const header = `🎒 **Opplevelser i Norge** — ${data.count} treff${applied ? ` (${applied})` : ""}:\n`;
|
|
182
|
+
const results = data.results.map((exp, i) => formatExperience(exp, i + 1)).join("\n\n");
|
|
183
|
+
|
|
184
|
+
return { content: [{ type: "text", text: header + "\n" + results }] };
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Tool 2: Full detail for a single experience by id
|
|
189
|
+
server.registerTool(
|
|
190
|
+
"opplev_info",
|
|
191
|
+
{
|
|
192
|
+
title: "Experience details",
|
|
193
|
+
description:
|
|
194
|
+
"Get the full profile for one experience by its id (UUID from opplev_discover results): " +
|
|
195
|
+
"title, description, category, county/municipality, indoor/outdoor, season, duration, group size, " +
|
|
196
|
+
"age suitability, price, languages, accessibility, meeting point and booking link.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
id: z.string().describe("Experience id (UUID). Get this from opplev_discover results."),
|
|
199
|
+
},
|
|
200
|
+
annotations: {
|
|
201
|
+
title: "Experience details",
|
|
202
|
+
readOnlyHint: true,
|
|
203
|
+
destructiveHint: false,
|
|
204
|
+
idempotentHint: true,
|
|
205
|
+
openWorldHint: true,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
async ({ id }) => {
|
|
209
|
+
const data = await fetchJSON(`${BASE_URL}/api/opplevelser/${id}`);
|
|
210
|
+
const exp = data.experience || data;
|
|
211
|
+
|
|
212
|
+
const sections = [`# ${exp.title}`];
|
|
213
|
+
|
|
214
|
+
const place = [exp.kommune, exp.fylke].filter(Boolean).join(", ");
|
|
215
|
+
const head = [];
|
|
216
|
+
if (place) head.push(`📍 ${place}`);
|
|
217
|
+
if (exp.category) head.push(categoryLabel(exp.category));
|
|
218
|
+
if (exp.confidence) head.push(`tillit: ${exp.confidence}`);
|
|
219
|
+
if (head.length) sections.push(head.join(" · "));
|
|
220
|
+
|
|
221
|
+
if (exp.description) sections.push(`\n${exp.description}`);
|
|
222
|
+
|
|
223
|
+
// Quick facts
|
|
224
|
+
const facts = [];
|
|
225
|
+
if (exp.indoor_outdoor) {
|
|
226
|
+
const io = { indoor: "Innendørs", outdoor: "Utendørs", both: "Inne/ute" }[exp.indoor_outdoor] || exp.indoor_outdoor;
|
|
227
|
+
facts.push(`- **Inne/ute:** ${io}`);
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(exp.season) && exp.season.length) facts.push(`- **Sesong:** ${exp.season.join(", ")}`);
|
|
230
|
+
const dur = durationText(exp);
|
|
231
|
+
if (dur) facts.push(`- **Varighet:** ${dur}`);
|
|
232
|
+
if (exp.group_min != null || exp.group_max != null)
|
|
233
|
+
facts.push(`- **Gruppe:** ${[exp.group_min, exp.group_max].filter((x) => x != null).join("–")} personer`);
|
|
234
|
+
if (exp.min_age != null) facts.push(`- **Min. alder:** ${exp.min_age}`);
|
|
235
|
+
if (exp.age_suitability) facts.push(`- **Passer for:** ${exp.age_suitability}`);
|
|
236
|
+
const price = priceText(exp);
|
|
237
|
+
if (price) facts.push(`- **Pris:** ${price}`);
|
|
238
|
+
if (Array.isArray(exp.languages) && exp.languages.length) facts.push(`- **Språk:** ${exp.languages.join(", ")}`);
|
|
239
|
+
if (Array.isArray(exp.accessibility) && exp.accessibility.length)
|
|
240
|
+
facts.push(`- **Tilgjengelighet:** ${exp.accessibility.join(", ")}`);
|
|
241
|
+
if (Array.isArray(exp.activity_tags) && exp.activity_tags.length)
|
|
242
|
+
facts.push(`- **Stikkord:** ${exp.activity_tags.join(", ")}`);
|
|
243
|
+
if (facts.length) sections.push(`\n## Fakta\n${facts.join("\n")}`);
|
|
244
|
+
|
|
245
|
+
if (exp.meeting_point) sections.push(`\n📌 **Oppmøte:** ${exp.meeting_point}`);
|
|
246
|
+
|
|
247
|
+
const links = [];
|
|
248
|
+
if (exp.slug) links.push(`🔗 [Profil](${profileUrl(exp.slug)})`);
|
|
249
|
+
if (exp.booking_url) links.push(`🎟️ [Book / mer info](${exp.booking_url})`);
|
|
250
|
+
if (exp.evidence_url && exp.evidence_url !== exp.booking_url) links.push(`📄 [Kilde](${exp.evidence_url})`);
|
|
251
|
+
if (links.length) sections.push(`\n${links.join(" · ")}`);
|
|
252
|
+
|
|
253
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Tool 3: List categories with counts
|
|
258
|
+
server.registerTool(
|
|
259
|
+
"opplev_categories",
|
|
260
|
+
{
|
|
261
|
+
title: "List experience categories",
|
|
262
|
+
description:
|
|
263
|
+
"List all experience categories on Opplevagent with the count of published experiences in each. " +
|
|
264
|
+
"Useful when the user asks 'what kinds of experiences are available?' or to pick a valid category " +
|
|
265
|
+
"value for opplev_discover.",
|
|
266
|
+
inputSchema: {},
|
|
267
|
+
annotations: {
|
|
268
|
+
title: "List experience categories",
|
|
269
|
+
readOnlyHint: true,
|
|
270
|
+
destructiveHint: false,
|
|
271
|
+
idempotentHint: true,
|
|
272
|
+
openWorldHint: true,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
async () => {
|
|
276
|
+
const data = await fetchJSON(`${BASE_URL}/api/opplevelser/categories`);
|
|
277
|
+
const cats = data.categories || [];
|
|
278
|
+
if (!cats.length) {
|
|
279
|
+
return { content: [{ type: "text", text: "Ingen kategorier registrert ennå." }] };
|
|
280
|
+
}
|
|
281
|
+
const total = cats.reduce((sum, c) => sum + (c.count || 0), 0);
|
|
282
|
+
const lines = [`🗂️ **Kategorier på Opplevagent** — ${total} opplevelser totalt:\n`];
|
|
283
|
+
for (const c of cats) {
|
|
284
|
+
lines.push(`- **${categoryLabel(c.category)}** (\`${c.category}\`) — ${c.count}`);
|
|
285
|
+
}
|
|
286
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ── Start ────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
const transport = new StdioServerTransport();
|
|
294
|
+
await server.connect(transport);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
main().catch((err) => {
|
|
298
|
+
console.error(err);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opplevagent-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.slookisen/opplevagent-mcp",
|
|
5
|
+
"description": "MCP server for Opplevagent — discover Norwegian experiences and activities from Claude Desktop. Search tours, courses and things to do by county, category, weather, season, group size, age and price.",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"opplevagent-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node test-smoke.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"claude",
|
|
21
|
+
"opplevagent",
|
|
22
|
+
"experiences",
|
|
23
|
+
"activities",
|
|
24
|
+
"things-to-do",
|
|
25
|
+
"tourism",
|
|
26
|
+
"norway",
|
|
27
|
+
"a2a",
|
|
28
|
+
"opplevelser"
|
|
29
|
+
],
|
|
30
|
+
"author": "Daniel Fredriksen",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
|
+
"zod": "^3.23.8"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/slookisen/opplevagent-mcp.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://opplevagent.no",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/slookisen/opplevagent-mcp/issues"
|
|
46
|
+
}
|
|
47
|
+
}
|