Forge
csharp3407750f
1using Tomlyn;
2using Tomlyn.Model;
3
4namespace AgentForge.Mcp;
5
6public static class ForgeMcpConfigLoader
7{
8 public static ForgeMcpLoadResult Load(string[] args)
9 {
10 string? configPath = null;
11 for (var i = 0; i < args.Length; i++)
12 {
13 var arg = args[i];
14 if (arg is "--config" or "-c")
15 {
16 if (i + 1 >= args.Length)
17 return ForgeMcpLoadResult.Fail("--config requires a file path.");
18
19 configPath = args[++i];
20 continue;
21 }
22
23 if (arg.StartsWith("--config=", StringComparison.Ordinal))
24 {
25 configPath = arg["--config=".Length..];
26 continue;
27 }
28
29 if (arg is "--help" or "-h")
30 return ForgeMcpLoadResult.Help();
31 }
32
33 ForgeMcpTomlValues? toml = null;
34 if (!string.IsNullOrWhiteSpace(configPath))
35 {
36 var path = Path.GetFullPath(configPath.Trim());
37 if (!File.Exists(path))
38 return ForgeMcpLoadResult.Fail($"Config file not found: {path}");
39
40 try
41 {
42 toml = ForgeMcpTomlParser.Parse(File.ReadAllText(path));
43 }
44 catch (Exception ex)
45 {
46 return ForgeMcpLoadResult.Fail($"Invalid TOML config '{path}': {ex.Message}");
47 }
48 }
49
50 var baseUrl = FirstNonEmpty(
51 Environment.GetEnvironmentVariable("FORGE_BASE_URL"),
52 toml?.BaseUrl,
53 ForgeMcpSettings.DefaultBaseUrl);
54
55 if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var baseUri))
56 return ForgeMcpLoadResult.Fail("forge base_url / FORGE_BASE_URL must be an absolute URL.");
57
58 var apiToken = FirstNonEmpty(
59 Environment.GetEnvironmentVariable("FORGE_API_TOKEN"),
60 toml?.ApiToken);
61
62 var actor = FirstNonEmpty(
63 Environment.GetEnvironmentVariable("FORGE_ACTOR"),
64 toml?.Actor);
65
66 var toolsListChanged = ReadBoolEnv("FORGE_MCP_TOOLS_LIST_CHANGED")
67 ?? toml?.ToolsListChanged
68 ?? false;
69
70 var pollSeconds = ReadIntEnv("FORGE_MCP_CAPABILITIES_POLL_SECONDS")
71 ?? toml?.CapabilitiesPollSeconds
72 ?? 0;
73
74 var syncCursorTools = ReadBoolEnv("FORGE_CURSOR_SYNC_TOOLS")
75 ?? ReadBoolEnv("FORGE_MCP_SYNC_CURSOR_TOOLS")
76 ?? toml?.SyncCursorToolDescriptors
77 ?? false;
78
79 var cursorToolsDir = FirstNonEmpty(
80 Environment.GetEnvironmentVariable("FORGE_CURSOR_TOOLS_DIR"),
81 toml?.CursorToolDescriptorsDir);
82
83 return ForgeMcpLoadResult.Ok(new ForgeMcpSettings
84 {
85 BaseUri = baseUri,
86 ApiToken = apiToken,
87 Actor = actor,
88 ToolsListChanged = toolsListChanged,
89 CapabilitiesPollSeconds = pollSeconds,
90 SyncCursorToolDescriptors = syncCursorTools,
91 CursorToolDescriptorsDir = cursorToolsDir,
92 });
93 }
94
95 private static string? FirstNonEmpty(params string?[] values)
96 {
97 foreach (var value in values)
98 {
99 if (!string.IsNullOrWhiteSpace(value))
100 return value.Trim();
101 }
102
103 return null;
104 }
105
106 private static bool? ReadBoolEnv(string name)
107 {
108 var raw = Environment.GetEnvironmentVariable(name);
109 if (string.IsNullOrWhiteSpace(raw))
110 return null;
111
112 return raw.Trim() switch
113 {
114 "1" or "true" or "True" or "yes" or "YES" => true,
115 "0" or "false" or "False" or "no" or "NO" => false,
116 _ => null,
117 };
118 }
119
120 private static int? ReadIntEnv(string name)
121 {
122 var raw = Environment.GetEnvironmentVariable(name);
123 return int.TryParse(raw, out var value) ? value : null;
124 }
125}
126
127public sealed class ForgeMcpLoadResult
128{
129 public ForgeMcpSettings? Settings { get; init; }
130
131 public string? Error { get; init; }
132
133 public bool IsHelp { get; init; }
134
135 public bool IsSuccess => Settings is not null && Error is null;
136
137 public static ForgeMcpLoadResult Ok(ForgeMcpSettings settings) => new() { Settings = settings };
138
139 public static ForgeMcpLoadResult Fail(string error) => new() { Error = error };
140
141 public static ForgeMcpLoadResult Help() => new() { IsHelp = true };
142}
143
144internal sealed class ForgeMcpTomlValues
145{
146 public string? BaseUrl { get; init; }
147
148 public string? ApiToken { get; init; }
149
150 public string? Actor { get; init; }
151
152 public bool? ToolsListChanged { get; init; }
153
154 public int? CapabilitiesPollSeconds { get; init; }
155
156 public bool? SyncCursorToolDescriptors { get; init; }
157
158 public string? CursorToolDescriptorsDir { get; init; }
159}
160
161internal static class ForgeMcpTomlParser
162{
163 internal const int SupportedVersion = 1;
164
165 internal static ForgeMcpTomlValues Parse(string text)
166 {
167 var model = Toml.ToModel(text);
168 var version = ReadInt(model, "version");
169 if (version is not null && version != SupportedVersion)
170 throw new InvalidOperationException($"Unsupported version {version}; expected {SupportedVersion}.");
171
172 var forge = ReadTable(model, "forge");
173 var identity = ReadTable(model, "identity");
174 var mcp = ReadTable(model, "mcp");
175 var cursor = ReadTable(model, "cursor");
176
177 return new ForgeMcpTomlValues
178 {
179 BaseUrl = ReadString(forge, "base_url"),
180 ApiToken = ReadString(identity, "api_token") ?? ReadString(forge, "api_token"),
181 Actor = ReadString(identity, "actor"),
182 ToolsListChanged = ReadBool(mcp, "tools_list_changed"),
183 CapabilitiesPollSeconds = ReadInt(mcp, "capabilities_poll_seconds"),
184 SyncCursorToolDescriptors = ReadBool(cursor, "sync_tool_descriptors"),
185 CursorToolDescriptorsDir = ReadString(cursor, "tool_descriptors_dir"),
186 };
187 }
188
189 private static TomlTable? ReadTable(TomlTable model, string key) =>
190 model.TryGetValue(key, out var value) && value is TomlTable table ? table : null;
191
192 private static string? ReadString(TomlTable? table, string key)
193 {
194 if (table is null || !table.TryGetValue(key, out var value) || value is null)
195 return null;
196
197 return value switch
198 {
199 string s => s.Trim(),
200 _ => value.ToString()?.Trim(),
201 };
202 }
203
204 private static int? ReadInt(TomlTable? table, string key)
205 {
206 if (table is null || !table.TryGetValue(key, out var value) || value is null)
207 return null;
208
209 return value switch
210 {
211 int i => i,
212 long l => (int)l,
213 string s when int.TryParse(s, out var parsed) => parsed,
214 _ => throw new InvalidOperationException($"Expected integer for '{key}'."),
215 };
216 }
217
218 private static bool? ReadBool(TomlTable? table, string key)
219 {
220 if (table is null || !table.TryGetValue(key, out var value) || value is null)
221 return null;
222
223 return value switch
224 {
225 bool b => b,
226 string s when bool.TryParse(s, out var parsed) => parsed,
227 string s when s is "1" or "0" => s == "1",
228 _ => throw new InvalidOperationException($"Expected boolean for '{key}'."),
229 };
230 }
231}
232
View only · write via MCP/CIDE